<?php

declare(strict_types=1);

namespace Drupal\Tests\migrate\Unit;

use Drupal\sqlite\Driver\Database\sqlite\Connection;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\migrate\Plugin\MigrateIdMapInterface;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\Tests\UnitTestCase;

/**
 * Provides setup and helper methods for Migrate module tests.
 */
abstract class MigrateTestCase extends UnitTestCase {

  /**
   * An array of migration configuration values.
   *
   * @var array
   */
  protected $migrationConfiguration = [];

  /**
   * The migration ID map.
   *
   * @var \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit\Framework\MockObject\MockObject
   */
  protected $idMap;

  /**
   * Local store for mocking setStatus()/getStatus().
   *
   * @var int
   */
  protected $migrationStatus = MigrationInterface::STATUS_IDLE;

  /**
   * Retrieves a mocked migration.
   *
   * @param \Drupal\migrate\Plugin\MigrateIdMapInterface|\PHPUnit\Framework\MockObject\MockObject|null $id_map
   *   An ID map plugin to use, or NULL for using a mocked one. Optional,
   *   defaults to NULL.
   *
   * @return \Drupal\migrate\Plugin\MigrationInterface|\PHPUnit\Framework\MockObject\MockObject
   *   The mocked migration.
   */
  protected function getMigration($id_map = NULL) {
    $this->migrationConfiguration += ['migrationClass' => 'Drupal\migrate\Plugin\Migration'];
    $this->idMap = $id_map;
    if (is_null($id_map)) {
      $this->idMap = $this->createMock(MigrateIdMapInterface::class);
      $this->idMap
        ->method('getQualifiedMapTableName')
        ->willReturn('test_map');
    }

    $migration = $this->getMockBuilder($this->migrationConfiguration['migrationClass'])
      ->disableOriginalConstructor()
      ->getMock();

    $migration->method('checkRequirements')
      ->willReturn(TRUE);

    $migration->method('getIdMap')
      ->willReturn($this->idMap);

    // We need the state to be toggled throughout the test so we store the value
    // on the test class and use a return callback.
    $migration->expects($this->any())
      ->method('getStatus')
      ->willReturnCallback(function () {
        return $this->migrationStatus;
      });
    $migration->expects($this->any())
      ->method('setStatus')
      ->willReturnCallback(function ($status) {
        $this->migrationStatus = $status;
      });

    $migration->method('getMigrationDependencies')
      ->willReturn([
        'required' => [],
        'optional' => [],
      ]);

    $configuration = &$this->migrationConfiguration;

    $migration->method('set')
      ->willReturnCallback(function ($argument, $value) use (&$configuration) {
        $configuration[$argument] = $value;
      });

    $migration->method('id')
      ->willReturn($configuration['id']);

    return $migration;
  }

  /**
   * Gets an SQLite database connection object for use in tests.
   *
   * @param array $database_contents
   *   The database contents faked as an array. Each key is a table name, each
   *   value is a list of table rows, an associative array of field => value.
   * @param array $connection_options
   *   (optional) Options for the database connection. Defaults to an empty
   *   array.
   *
   * @return \Drupal\sqlite\Driver\Database\sqlite\Connection
   *   The database connection.
   */
  protected function getDatabase(array $database_contents, $connection_options = []) {
    if (extension_loaded('pdo_sqlite')) {
      $connection_options['database'] = ':memory:';
      $pdo = Connection::open($connection_options);
      $connection = new Connection($pdo, $connection_options);
    }
    else {
      $this->markTestSkipped('The pdo_sqlite extension is not available.');
    }

    // Initialize the DIC with a fake module handler for alterable queries.
    $container = new ContainerBuilder();
    $container->set('module_handler', $this->createMock('\Drupal\Core\Extension\ModuleHandlerInterface'));
    \Drupal::setContainer($container);

    // Create the tables and load them up with data, skipping empty ones.
    foreach (array_filter($database_contents) as $table => $rows) {
      $pilot_row = reset($rows);
      $connection->schema()->createTable($table, $this->createSchemaFromRow($pilot_row));

      $insert = $connection->insert($table)->fields(array_keys($pilot_row));
      array_walk($rows, [$insert, 'values']);
      $insert->execute();
    }

    return $connection;
  }

  /**
   * Generates a table schema from a row.
   *
   * @param array $row
   *   The reference row on which to base the schema.
   *
   * @return array
   *   The Schema API-ready table schema.
   */
  protected function createSchemaFromRow(array $row) {
    // SQLite uses loose ("affinity") typing, so it is OK for every column to be
    // a text field.
    $fields = array_map(function () {
      return ['type' => 'text'];
    }, $row);
    return ['fields' => $fields];
  }

  /**
   * Tests a query.
   *
   * @param array|\Traversable $iter
   *   The countable. foreach-able actual results if a query is being run.
   * @param array $expected_results
   *   An array of expected results.
   */
  public function queryResultTest($iter, $expected_results) {
    $this->assertSameSize($expected_results, $iter, 'Number of results match');
    $count = 0;
    foreach ($iter as $data_row) {
      $expected_row = $expected_results[$count];
      $count++;
      foreach ($expected_row as $key => $expected_value) {
        $this->retrievalAssertHelper($expected_value, $this->getValue($data_row, $key), sprintf('Value matches for key "%s"', $key));
      }
    }
    $this->assertSame(count($expected_results), $count);
  }

  /**
   * Gets the value on a row for a given key.
   *
   * @param array $row
   *   The row information.
   * @param string $key
   *   The key identifier.
   *
   * @return mixed
   *   The value on a row for a given key.
   */
  protected function getValue($row, $key) {
    return $row[$key];
  }

  /**
   * Asserts tested values during test retrieval.
   *
   * @param mixed $expected_value
   *   The incoming expected value to test.
   * @param mixed $actual_value
   *   The incoming value itself.
   * @param string $message
   *   The tested result as a formatted string.
   */
  protected function retrievalAssertHelper($expected_value, $actual_value, $message) {
    if (is_array($expected_value)) {
      // If the expected and actual values are empty, no need to array compare.
      if (empty($expected_value && $actual_value)) {
        return;
      }
      $this->assertEquals($expected_value, $actual_value, $message);
    }
    else {
      $this->assertSame((string) $expected_value, (string) $actual_value, $message);
    }
  }

}
