<?php

/**
 * @file
 * Contains \Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTest.
 */

namespace Drupal\Tests\Core\Entity\Sql;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\Query\QueryFactoryInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Language\Language;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * @coversDefaultClass \Drupal\Core\Entity\Sql\SqlContentEntityStorage
 * @group Entity
 */
class SqlContentEntityStorageTest extends UnitTestCase {

  /**
   * The content entity database storage used in this test.
   *
   * @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $entityStorage;

  /**
   * The mocked entity type used in this test.
   *
   * @var \Drupal\Core\Entity\ContentEntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $entityType;

  /**
   * An array of field definitions used for this test, keyed by field name.
   *
   * @var \Drupal\Core\Field\BaseFieldDefinition[]|\PHPUnit_Framework_MockObject_MockObject[]
   */
  protected $fieldDefinitions = [];

  /**
   * The mocked entity manager used in this test.
   *
   * @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $entityManager;

  /**
   * The entity type ID.
   *
   * @var string
   */
  protected $entityTypeId = 'entity_test';

  /**
   * The dependency injection container.
   *
   * @var \Symfony\Component\DependencyInjection\ContainerBuilder
   */
  protected $container;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $moduleHandler;

  /**
   * The cache backend to use.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $cache;

  /**
   * The language manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $languageManager;

  /**
   * The database connection to use.
   *
   * @var \Drupal\Core\Database\Connection|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $connection;

  /**
   * {@inheritdoc}
   */
  protected function setUp() {
    $this->entityType = $this->getMock('Drupal\Core\Entity\ContentEntityTypeInterface');
    $this->entityType->expects($this->any())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));

    $this->container = new ContainerBuilder();
    \Drupal::setContainer($this->container);

    $this->entityManager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
    $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
    $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface');
    $this->languageManager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');
    $this->languageManager->expects($this->any())
      ->method('getDefaultLanguage')
      ->will($this->returnValue(new Language(['langcode' => 'en'])));
    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
      ->disableOriginalConstructor()
      ->getMock();
  }

  /**
   * Tests SqlContentEntityStorage::getBaseTable().
   *
   * @param string $base_table
   *   The base table to be returned by the mocked entity type.
   * @param string $expected
   *   The expected return value of
   *   SqlContentEntityStorage::getBaseTable().
   *
   * @covers ::__construct
   * @covers ::getBaseTable
   *
   * @dataProvider providerTestGetBaseTable
   */
  public function testGetBaseTable($base_table, $expected) {
    $this->entityType->expects($this->once())
      ->method('getBaseTable')
      ->willReturn($base_table);

    $this->setUpEntityStorage();

    $this->assertSame($expected, $this->entityStorage->getBaseTable());
  }

  /**
   * Provides test data for testGetBaseTable().
   *
   * @return array[]
   *   An nested array where each inner array has the base table to be returned
   *   by the mocked entity type as the first value and the expected return
   *   value of SqlContentEntityStorage::getBaseTable() as the second
   *   value.
   */
  public function providerTestGetBaseTable() {
    return [
      // Test that the entity type's base table is used, if provided.
      ['entity_test', 'entity_test'],
      // Test that the storage falls back to the entity type ID.
      [NULL, 'entity_test'],
    ];
  }

  /**
   * Tests SqlContentEntityStorage::getRevisionTable().
   *
   * @param string $revision_table
   *   The revision table to be returned by the mocked entity type.
   * @param string $expected
   *   The expected return value of
   *   SqlContentEntityStorage::getRevisionTable().
   *
   * @covers ::__construct
   * @covers ::getRevisionTable
   *
   * @dataProvider providerTestGetRevisionTable
   */
  public function testGetRevisionTable($revision_table, $expected) {
    $this->entityType->expects($this->once())
      ->method('isRevisionable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->once())
      ->method('getRevisionTable')
      ->will($this->returnValue($revision_table));

    $this->setUpEntityStorage();

    $this->assertSame($expected, $this->entityStorage->getRevisionTable());
  }

  /**
   * Provides test data for testGetRevisionTable().
   *
   * @return array[]
   *   An nested array where each inner array has the revision table to be
   *   returned by the mocked entity type as the first value and the expected
   *   return value of SqlContentEntityStorage::getRevisionTable() as the
   *   second value.
   */
  public function providerTestGetRevisionTable() {
    return [
      // Test that the entity type's revision table is used, if provided.
      ['entity_test_revision', 'entity_test_revision'],
      // Test that the storage falls back to the entity type ID with a
      // '_revision' suffix.
      [NULL, 'entity_test_revision'],
    ];
  }

  /**
   * Tests SqlContentEntityStorage::getDataTable().
   *
   * @covers ::__construct
   * @covers ::getDataTable
   */
  public function testGetDataTable() {
    $this->entityType->expects($this->once())
      ->method('isTranslatable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->exactly(1))
      ->method('getDataTable')
      ->will($this->returnValue('entity_test_field_data'));

    $this->setUpEntityStorage();

    $this->assertSame('entity_test_field_data', $this->entityStorage->getDataTable());
  }

  /**
   * Tests SqlContentEntityStorage::getRevisionDataTable().
   *
   * @param string $revision_data_table
   *   The revision data table to be returned by the mocked entity type.
   * @param string $expected
   *   The expected return value of
   *   SqlContentEntityStorage::getRevisionDataTable().
   *
   * @covers ::__construct
   * @covers ::getRevisionDataTable
   *
   * @dataProvider providerTestGetRevisionDataTable
   */
  public function testGetRevisionDataTable($revision_data_table, $expected) {
    $this->entityType->expects($this->once())
      ->method('isRevisionable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->once())
      ->method('isTranslatable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->exactly(1))
      ->method('getDataTable')
      ->will($this->returnValue('entity_test_field_data'));
    $this->entityType->expects($this->once())
      ->method('getRevisionDataTable')
      ->will($this->returnValue($revision_data_table));

    $this->setUpEntityStorage();

    $actual = $this->entityStorage->getRevisionDataTable();
    $this->assertSame($expected, $actual);
  }

  /**
   * Provides test data for testGetRevisionDataTable().
   *
   * @return array[]
   *   An nested array where each inner array has the revision data table to be
   *   returned by the mocked entity type as the first value and the expected
   *   return value of SqlContentEntityStorage::getRevisionDataTable() as
   *   the second value.
   */
  public function providerTestGetRevisionDataTable() {
    return [
      // Test that the entity type's revision data table is used, if provided.
      ['entity_test_field_revision', 'entity_test_field_revision'],
      // Test that the storage falls back to the entity type ID with a
      // '_field_revision' suffix.
      [NULL, 'entity_test_field_revision'],
    ];
  }

  /**
   * Tests ContentEntityDatabaseStorage::onEntityTypeCreate().
   *
   * @covers ::__construct
   * @covers ::onEntityTypeCreate
   * @covers ::getTableMapping
   */
  public function testOnEntityTypeCreate() {
    $columns = [
      'value' => [
        'type' => 'int',
      ],
    ];

    $this->fieldDefinitions = $this->mockFieldDefinitions(['id']);
    $this->fieldDefinitions['id']->expects($this->any())
      ->method('getColumns')
      ->will($this->returnValue($columns));
    $this->fieldDefinitions['id']->expects($this->once())
      ->method('getSchema')
      ->will($this->returnValue(['columns' => $columns]));

    $this->entityType->expects($this->once())
      ->method('getKeys')
      ->will($this->returnValue(['id' => 'id']));
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        // EntityStorageBase::__construct()
        ['id', 'id'],
        // ContentEntityStorageBase::__construct()
        ['uuid', NULL],
        ['bundle', NULL],
        // SqlContentEntityStorageSchema::initializeBaseTable()
        ['id' => 'id'],
        // SqlContentEntityStorageSchema::processBaseTable()
        ['id' => 'id'],
      ]));

    $this->setUpEntityStorage();

    $expected = [
      'description' => 'The base table for entity_test entities.',
      'fields' => [
        'id' => [
          'type' => 'serial',
          'not null' => TRUE,
        ],
      ],
      'primary key' => ['id'],
      'unique keys' => [],
      'indexes' => [],
      'foreign keys' => [],
    ];

    $schema_handler = $this->getMockBuilder('Drupal\Core\Database\Schema')
      ->disableOriginalConstructor()
      ->getMock();
    $schema_handler->expects($this->any())
      ->method('createTable')
      ->with($this->equalTo('entity_test'), $this->equalTo($expected));

    $this->connection->expects($this->once())
      ->method('schema')
      ->will($this->returnValue($schema_handler));

    $storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
      ->setConstructorArgs([$this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager])
      ->setMethods(['getStorageSchema'])
      ->getMock();

    $key_value = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreInterface');
    $schema_handler = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema')
      ->setConstructorArgs([$this->entityManager, $this->entityType, $storage, $this->connection])
      ->setMethods(['installedStorageSchema', 'createSharedTableSchema'])
      ->getMock();
    $schema_handler
      ->expects($this->any())
      ->method('installedStorageSchema')
      ->will($this->returnValue($key_value));

    $storage
      ->expects($this->any())
      ->method('getStorageSchema')
      ->will($this->returnValue($schema_handler));

    $storage->onEntityTypeCreate($this->entityType);
  }

  /**
   * Tests getTableMapping() with an empty entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   */
  public function testGetTableMappingEmpty() {
    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();
    $this->assertSame(['entity_test'], $mapping->getTableNames());
    $this->assertSame([], $mapping->getFieldNames('entity_test'));
    $this->assertSame([], $mapping->getExtraColumns('entity_test'));
  }

  /**
   * Tests getTableMapping() with a simple entity type.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingSimple(array $entity_keys) {
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        ['id', $entity_keys['id']],
        ['uuid', $entity_keys['uuid']],
        ['bundle', $entity_keys['bundle']],
      ]));

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();

    $this->assertEquals(['entity_test'], $mapping->getTableNames());

    $expected = array_values(array_filter($entity_keys));
    $this->assertEquals($expected, $mapping->getFieldNames('entity_test'));

    $this->assertEquals([], $mapping->getExtraColumns('entity_test'));
  }

  /**
   * Tests getTableMapping() with a simple entity type with some base fields.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingSimpleWithFields(array $entity_keys) {
    $base_field_names = ['title', 'description', 'owner'];
    $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);
    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();
    $this->assertEquals(['entity_test'], $mapping->getTableNames());
    $this->assertEquals($field_names, $mapping->getFieldNames('entity_test'));
    $this->assertEquals([], $mapping->getExtraColumns('entity_test'));
  }

  /**
   * Provides test data for testGetTableMappingSimple().
   *
   * @return array[]
   *   A nested array, where each inner array has a single value being a  map of
   *   entity keys to use for the mocked entity type.
   */
  public function providerTestGetTableMappingSimple() {
    return [
      [['id' => 'test_id', 'bundle' => NULL, 'uuid' => NULL]],
      [['id' => 'test_id', 'bundle' => 'test_bundle', 'uuid' => NULL]],
      [['id' => 'test_id', 'bundle' => NULL, 'uuid' => 'test_uuid']],
      [['id' => 'test_id', 'bundle' => 'test_bundle', 'uuid' => 'test_uuid']],
    ];
  }

  /**
   * Tests getTableMapping() with a base field that requires a dedicated table.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   */
  public function testGetTableMappingSimpleWithDedicatedStorageFields() {
    $base_field_names = ['multi_valued_base_field'];

    // Set up one entity key in order to have a base table.
    $this->fieldDefinitions = $this->mockFieldDefinitions(['test_id']);

    // Set up the multi-valued base field.
    $this->fieldDefinitions += $this->mockFieldDefinitions($base_field_names, [
      'hasCustomStorage' => FALSE,
      'isMultiple' => TRUE,
      'getTargetEntityTypeId' => 'entity_test',
    ]);

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();
    $this->assertEquals(['entity_test', 'entity_test__multi_valued_base_field'], $mapping->getTableNames());
    $this->assertEquals($base_field_names, $mapping->getFieldNames('entity_test__multi_valued_base_field'));

    $extra_columns = [
      'bundle',
      'deleted',
      'entity_id',
      'revision_id',
      'langcode',
      'delta',
    ];
    $this->assertEquals($extra_columns, $mapping->getExtraColumns('entity_test__multi_valued_base_field'));
  }

  /**
   * Tests getTableMapping() with a revisionable, non-translatable entity type.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingRevisionable(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys = [
      'id' => $entity_keys['id'],
      'revision' => 'test_revision',
      'bundle' => $entity_keys['bundle'],
      'uuid' => $entity_keys['uuid'],
    ];

    $this->entityType->expects($this->exactly(2))
      ->method('isRevisionable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        ['id', $entity_keys['id']],
        ['uuid', $entity_keys['uuid']],
        ['bundle', $entity_keys['bundle']],
        ['revision', $entity_keys['revision']],
      ]));
    $this->entityType->expects($this->any())
      ->method('getRevisionMetadataKeys')
      ->will($this->returnValue([]));

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();

    $expected = ['entity_test', 'entity_test_revision'];
    $this->assertEquals($expected, $mapping->getTableNames());

    $expected = array_values(array_filter($entity_keys));
    $this->assertEquals($expected, $mapping->getFieldNames('entity_test'));
    $expected = [$entity_keys['id'], $entity_keys['revision']];
    $this->assertEquals($expected, $mapping->getFieldNames('entity_test_revision'));

    $this->assertEquals([], $mapping->getExtraColumns('entity_test'));
    $this->assertEquals([], $mapping->getExtraColumns('entity_test_revision'));
  }

  /**
   * Tests getTableMapping() with a revisionable entity type with fields.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingRevisionableWithFields(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys = [
      'id' => $entity_keys['id'],
      'revision' => 'test_revision',
      'bundle' => $entity_keys['bundle'],
      'uuid' => $entity_keys['uuid'],
    ];

    // PHPUnit does not allow for multiple data providers.
    $test_cases = [
      [],
      ['revision_created' => 'revision_timestamp'],
      ['revision_user' => 'revision_uid'],
      ['revision_log_message' => 'revision_log'],
      ['revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid'],
      ['revision_created' => 'revision_timestamp', 'revision_log_message' => 'revision_log'],
      ['revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'],
      ['revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'],
    ];
    foreach ($test_cases as $revision_metadata_field_names) {
      $this->setUp();

      $base_field_names = ['title'];
      $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);

      $revisionable_field_names = ['description', 'owner'];
      $field_names = array_merge($field_names, $revisionable_field_names);
      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, array_values($revision_metadata_field_names)), ['isRevisionable' => TRUE]);

      $this->entityType->expects($this->exactly(2))
        ->method('isRevisionable')
        ->will($this->returnValue(TRUE));
      $this->entityType->expects($this->any())
        ->method('getKey')
        ->will($this->returnValueMap([
          ['id', $entity_keys['id']],
          ['uuid', $entity_keys['uuid']],
          ['bundle', $entity_keys['bundle']],
          ['revision', $entity_keys['revision']],
        ]));

      $this->entityType->expects($this->any())
        ->method('getRevisionMetadataKeys')
        ->will($this->returnValue($revision_metadata_field_names));

      $this->setUpEntityStorage();

      $mapping = $this->entityStorage->getTableMapping();

      $expected = ['entity_test', 'entity_test_revision'];
      $this->assertEquals($expected, $mapping->getTableNames());

      $this->assertEquals($field_names, $mapping->getFieldNames('entity_test'));
      $expected = array_merge(
        [$entity_keys['id'], $entity_keys['revision']],
        $revisionable_field_names,
        array_values($revision_metadata_field_names)
      );
      $this->assertEquals($expected, $mapping->getFieldNames('entity_test_revision'));

      $this->assertEquals([], $mapping->getExtraColumns('entity_test'));
      $this->assertEquals([], $mapping->getExtraColumns('entity_test_revision'));
    }
  }

  /**
   * Tests getTableMapping() with a non-revisionable, translatable entity type.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingTranslatable(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys['langcode'] = 'langcode';

    $this->entityType->expects($this->atLeastOnce())
      ->method('isTranslatable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getDataTable')
      ->will($this->returnValue('entity_test_field_data'));
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        ['id', $entity_keys['id']],
        ['uuid', $entity_keys['uuid']],
        ['bundle', $entity_keys['bundle']],
        ['langcode', $entity_keys['langcode']],
      ]));

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();

    $expected = ['entity_test', 'entity_test_field_data'];
    $this->assertEquals($expected, $mapping->getTableNames());

    $expected = array_values(array_filter($entity_keys));
    $actual = $mapping->getFieldNames('entity_test');
    $this->assertEquals($expected, $actual);
    // The UUID is not stored on the data table.
    $expected = array_values(array_filter([
      $entity_keys['id'],
      $entity_keys['bundle'],
      $entity_keys['langcode'],
    ]));
    $actual = $mapping->getFieldNames('entity_test_field_data');
    $this->assertEquals($expected, $actual);

    $expected = [];
    $actual = $mapping->getExtraColumns('entity_test');
    $this->assertEquals($expected, $actual);
    $actual = $mapping->getExtraColumns('entity_test_field_data');
    $this->assertEquals($expected, $actual);
  }

  /**
   * Tests getTableMapping() with a translatable entity type with fields.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingTranslatableWithFields(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys['langcode'] = 'langcode';

    $base_field_names = ['title', 'description', 'owner'];
    $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
    $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);

    $this->entityType->expects($this->atLeastOnce())
      ->method('isTranslatable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getDataTable')
      ->will($this->returnValue('entity_test_field_data'));
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        ['id', $entity_keys['id']],
        ['uuid', $entity_keys['uuid']],
        ['bundle', $entity_keys['bundle']],
        ['langcode', $entity_keys['langcode']],
      ]));

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();

    $expected = ['entity_test', 'entity_test_field_data'];
    $this->assertEquals($expected, $mapping->getTableNames());

    $expected = array_values(array_filter($entity_keys));
    $actual = $mapping->getFieldNames('entity_test');
    $this->assertEquals($expected, $actual);
    // The UUID is not stored on the data table.
    $expected = array_merge(array_filter([
      $entity_keys['id'],
      $entity_keys['bundle'],
      $entity_keys['langcode'],
    ]), $base_field_names);
    $actual = $mapping->getFieldNames('entity_test_field_data');
    $this->assertEquals($expected, $actual);

    $expected = [];
    $actual = $mapping->getExtraColumns('entity_test');
    $this->assertEquals($expected, $actual);
    $actual = $mapping->getExtraColumns('entity_test_field_data');
    $this->assertEquals($expected, $actual);
  }

  /**
   * Tests getTableMapping() with a revisionable, translatable entity type.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingRevisionableTranslatable(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys = [
      'id' => $entity_keys['id'],
      'revision' => 'test_revision',
      'bundle' => $entity_keys['bundle'],
      'uuid' => $entity_keys['uuid'],
      'langcode' => 'langcode',
    ];
    $revision_metadata_keys = [
      'revision_created' => 'revision_timestamp',
      'revision_user' => 'revision_uid',
      'revision_log_message' => 'revision_log'
    ];

    $this->entityType->expects($this->atLeastOnce())
      ->method('isRevisionable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('isTranslatable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getDataTable')
      ->will($this->returnValue('entity_test_field_data'));
    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap([
        ['id', $entity_keys['id']],
        ['uuid', $entity_keys['uuid']],
        ['bundle', $entity_keys['bundle']],
        ['revision', $entity_keys['revision']],
        ['langcode', $entity_keys['langcode']],
      ]));
    $this->entityType->expects($this->any())
      ->method('getRevisionMetadataKeys')
      ->will($this->returnValue($revision_metadata_keys));

    $this->setUpEntityStorage();

    $mapping = $this->entityStorage->getTableMapping();

    $expected = [
      'entity_test',
      'entity_test_field_data',
      'entity_test_revision',
      'entity_test_field_revision',
    ];
    $this->assertEquals($expected, $mapping->getTableNames());

    // The default language code is stored on the base table.
    $expected = array_values(array_filter([
      $entity_keys['id'],
      $entity_keys['revision'],
      $entity_keys['bundle'],
      $entity_keys['uuid'],
      $entity_keys['langcode'],
    ]));
    $actual = $mapping->getFieldNames('entity_test');
    $this->assertEquals($expected, $actual);
    // The revision table on the other hand does not store the bundle and the
    // UUID.
    $expected = array_values(array_filter([
      $entity_keys['id'],
      $entity_keys['revision'],
      $entity_keys['langcode'],
    ]));
    $expected = array_merge($expected, array_values($revision_metadata_keys));
    $actual = $mapping->getFieldNames('entity_test_revision');
    $this->assertEquals($expected, $actual);
    // The UUID is not stored on the data table.
    $expected = array_values(array_filter([
      $entity_keys['id'],
      $entity_keys['revision'],
      $entity_keys['bundle'],
      $entity_keys['langcode'],
    ]));
    $actual = $mapping->getFieldNames('entity_test_field_data');
    $this->assertEquals($expected, $actual);
    // The data revision also does not store the bundle.
    $expected = array_values(array_filter([
      $entity_keys['id'],
      $entity_keys['revision'],
      $entity_keys['langcode'],
    ]));
    $actual = $mapping->getFieldNames('entity_test_field_revision');
    $this->assertEquals($expected, $actual);

    $expected = [];
    $actual = $mapping->getExtraColumns('entity_test');
    $this->assertEquals($expected, $actual);
    $actual = $mapping->getExtraColumns('entity_test_revision');
    $this->assertEquals($expected, $actual);
    $actual = $mapping->getExtraColumns('entity_test_field_data');
    $this->assertEquals($expected, $actual);
    $actual = $mapping->getExtraColumns('entity_test_field_revision');
    $this->assertEquals($expected, $actual);
  }

  /**
   * Tests getTableMapping() with a complex entity type with fields.
   *
   * @param string[] $entity_keys
   *   A map of entity keys to use for the mocked entity type.
   *
   * @covers ::__construct
   * @covers ::getTableMapping
   *
   * @dataProvider providerTestGetTableMappingSimple()
   */
  public function testGetTableMappingRevisionableTranslatableWithFields(array $entity_keys) {
    // This allows to re-use the data provider.
    $entity_keys = [
      'id' => $entity_keys['id'],
      'revision' => 'test_revision',
      'bundle' => $entity_keys['bundle'],
      'uuid' => $entity_keys['uuid'],
      'langcode' => 'langcode',
    ];

    // PHPUnit does not allow for multiple data providers.
    $test_cases = [
      [],
      ['revision_created' => 'revision_timestamp'],
      ['revision_user' => 'revision_uid'],
      ['revision_log_message' => 'revision_log'],
      ['revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid'],
      ['revision_created' => 'revision_timestamp', 'revision_log_message' => 'revision_log'],
      ['revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'],
      ['revision_created' => 'revision_timestamp', 'revision_user' => 'revision_uid', 'revision_log_message' => 'revision_log'],
    ];
    foreach ($test_cases as $revision_metadata_field_names) {
      $this->setUp();

      $base_field_names = ['title'];
      $field_names = array_merge(array_values(array_filter($entity_keys)), $base_field_names);
      $this->fieldDefinitions = $this->mockFieldDefinitions($field_names);

      $revisionable_field_names = ['description', 'owner'];
      $this->fieldDefinitions += $this->mockFieldDefinitions(array_merge($revisionable_field_names, array_values($revision_metadata_field_names)), ['isRevisionable' => TRUE]);

      $this->entityType->expects($this->atLeastOnce())
        ->method('isRevisionable')
        ->will($this->returnValue(TRUE));
      $this->entityType->expects($this->atLeastOnce())
        ->method('isTranslatable')
        ->will($this->returnValue(TRUE));
      $this->entityType->expects($this->atLeastOnce())
        ->method('getDataTable')
        ->will($this->returnValue('entity_test_field_data'));
      $this->entityType->expects($this->any())
        ->method('getKey')
        ->will($this->returnValueMap([
          ['id', $entity_keys['id']],
          ['uuid', $entity_keys['uuid']],
          ['bundle', $entity_keys['bundle']],
          ['revision', $entity_keys['revision']],
          ['langcode', $entity_keys['langcode']],
        ]));
      $this->entityType->expects($this->any())
        ->method('getRevisionMetadataKeys')
        ->will($this->returnValue($revision_metadata_field_names));

      $this->setUpEntityStorage();

      $mapping = $this->entityStorage->getTableMapping();

      $expected = [
        'entity_test',
        'entity_test_field_data',
        'entity_test_revision',
        'entity_test_field_revision',
      ];
      $this->assertEquals($expected, $mapping->getTableNames());

      $expected = [
        'entity_test',
        'entity_test_field_data',
        'entity_test_revision',
        'entity_test_field_revision',
      ];
      $this->assertEquals($expected, $mapping->getTableNames());

      // The default language code is not stored on the base table.
      $expected = array_values(array_filter([
        $entity_keys['id'],
        $entity_keys['revision'],
        $entity_keys['bundle'],
        $entity_keys['uuid'],
        $entity_keys['langcode'],
      ]));
      $actual = $mapping->getFieldNames('entity_test');
      $this->assertEquals($expected, $actual);
      // The revision table on the other hand does not store the bundle and the
      // UUID.
      $expected = array_merge(array_filter([
        $entity_keys['id'],
        $entity_keys['revision'],
        $entity_keys['langcode'],
      ]), array_values($revision_metadata_field_names));
      $actual = $mapping->getFieldNames('entity_test_revision');
      $this->assertEquals($expected, $actual);
      // The UUID is not stored on the data table.
      $expected = array_merge(array_filter([
        $entity_keys['id'],
        $entity_keys['revision'],
        $entity_keys['bundle'],
        $entity_keys['langcode'],
      ]), $base_field_names, $revisionable_field_names);
      $actual = $mapping->getFieldNames('entity_test_field_data');
      $this->assertEquals($expected, $actual);
      // The data revision also does not store the bundle.
      $expected = array_merge(array_filter([
        $entity_keys['id'],
        $entity_keys['revision'],
        $entity_keys['langcode'],
      ]), $revisionable_field_names);
      $actual = $mapping->getFieldNames('entity_test_field_revision');
      $this->assertEquals($expected, $actual);

      $expected = [];
      $actual = $mapping->getExtraColumns('entity_test');
      $this->assertEquals($expected, $actual);
      $actual = $mapping->getExtraColumns('entity_test_revision');
      $this->assertEquals($expected, $actual);
      $actual = $mapping->getExtraColumns('entity_test_field_data');
      $this->assertEquals($expected, $actual);
      $actual = $mapping->getExtraColumns('entity_test_field_revision');
      $this->assertEquals($expected, $actual);
    }
  }

  /**
   * @covers ::create
   */
  public function testCreate() {
    $language_manager = $this->getMock('Drupal\Core\Language\LanguageManagerInterface');

    $language = new Language(['id' => 'en']);
    $language_manager->expects($this->any())
      ->method('getCurrentLanguage')
      ->will($this->returnValue($language));

    $this->container->set('language_manager', $language_manager);
    $this->container->set('entity.manager', $this->entityManager);
    $this->container->set('module_handler', $this->moduleHandler);

    $entity = $this->getMockBuilder('Drupal\Core\Entity\ContentEntityBase')
      ->disableOriginalConstructor()
      ->setMethods(['id'])
      ->getMockForAbstractClass();

    $this->entityType->expects($this->atLeastOnce())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getClass')
      ->will($this->returnValue(get_class($entity)));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getKeys')
      ->will($this->returnValue(['id' => 'id']));

    // ContentEntityStorageBase iterates over the entity which calls this method
    // internally in ContentEntityBase::getProperties().
    $this->entityManager->expects($this->once())
      ->method('getFieldDefinitions')
      ->will($this->returnValue([]));

    $this->entityType->expects($this->atLeastOnce())
      ->method('isRevisionable')
      ->will($this->returnValue(FALSE));
    $this->entityManager->expects($this->atLeastOnce())
      ->method('getDefinition')
      ->with($this->entityType->id())
      ->will($this->returnValue($this->entityType));

    $this->setUpEntityStorage();

    $entity = $this->entityStorage->create();
    $entity->expects($this->atLeastOnce())
      ->method('id')
      ->will($this->returnValue('foo'));

    $this->assertInstanceOf('Drupal\Core\Entity\EntityInterface', $entity);
    $this->assertSame('foo', $entity->id());
    $this->assertTrue($entity->isNew());
  }

  /**
   * Returns a set of mock field definitions for the given names.
   *
   * @param array $field_names
   *   An array of field names.
   * @param array $methods
   *   (optional) An associative array of mock method return values keyed by
   *   method name.
   *
   * @return \Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface[]|\PHPUnit_Framework_MockObject_MockObject[]
   *   An array of mock base field definitions.
   */
  protected function mockFieldDefinitions(array $field_names, $methods = []) {
    $field_definitions = [];
    $definition = $this->getMock('Drupal\Tests\Core\Field\TestBaseFieldDefinitionInterface');

    // Assign common method return values.
    $methods += [
      'isBaseField' => TRUE,
    ];
    foreach ($methods as $method => $result) {
      $definition
        ->expects($this->any())
        ->method($method)
        ->will($this->returnValue($result));
    }

    // Assign field names to mock definitions.
    foreach ($field_names as $field_name) {
      $field_definitions[$field_name] = clone $definition;
      $field_definitions[$field_name]
        ->expects($this->any())
        ->method('getName')
        ->will($this->returnValue($field_name));
    }

    return $field_definitions;
  }

  /**
   * Sets up the content entity database storage.
   */
  protected function setUpEntityStorage() {
    $this->connection = $this->getMockBuilder('Drupal\Core\Database\Connection')
      ->disableOriginalConstructor()
      ->getMock();

    $this->entityManager->expects($this->any())
      ->method('getDefinition')
      ->will($this->returnValue($this->entityType));

    $this->entityManager->expects($this->any())
      ->method('getFieldStorageDefinitions')
      ->will($this->returnValue($this->fieldDefinitions));

    $this->entityManager->expects($this->any())
      ->method('getBaseFieldDefinitions')
      ->will($this->returnValue($this->fieldDefinitions));

    $this->entityStorage = new SqlContentEntityStorage($this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager);
  }

  /**
   * @covers ::doLoadMultiple
   * @covers ::buildCacheId
   * @covers ::getFromPersistentCache
   */
  public function testLoadMultiplePersistentCached() {
    $this->setUpModuleHandlerNoImplementations();

    $key = 'values:' . $this->entityTypeId . ':1';
    $id = 1;
    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTestEntityInterface')
      ->getMockForAbstractClass();
    $entity->expects($this->any())
      ->method('id')
      ->will($this->returnValue($id));

    $this->entityType->expects($this->atLeastOnce())
      ->method('isPersistentlyCacheable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getClass')
      ->will($this->returnValue(get_class($entity)));

    $this->cache->expects($this->once())
      ->method('getMultiple')
      ->with([$key])
      ->will($this->returnValue([$key => (object) ['data' => $entity]]));
    $this->cache->expects($this->never())
      ->method('set');

    $this->setUpEntityStorage();
    $entities = $this->entityStorage->loadMultiple([$id]);
    $this->assertEquals($entity, $entities[$id]);
  }

  /**
   * @covers ::doLoadMultiple
   * @covers ::buildCacheId
   * @covers ::getFromPersistentCache
   * @covers ::setPersistentCache
   */
  public function testLoadMultipleNoPersistentCache() {
    $this->setUpModuleHandlerNoImplementations();

    $id = 1;
    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTestEntityInterface')
      ->getMockForAbstractClass();
    $entity->expects($this->any())
      ->method('id')
      ->will($this->returnValue($id));

    $this->entityType->expects($this->any())
      ->method('isPersistentlyCacheable')
      ->will($this->returnValue(FALSE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getClass')
      ->will($this->returnValue(get_class($entity)));

    // There should be no calls to the cache backend for an entity type without
    // persistent caching.
    $this->cache->expects($this->never())
      ->method('getMultiple');
    $this->cache->expects($this->never())
      ->method('set');

    $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
      ->setConstructorArgs([$this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager])
      ->setMethods(['getFromStorage', 'invokeStorageLoadHook'])
      ->getMock();
    $entity_storage->method('invokeStorageLoadHook')
      ->willReturn(NULL);
    $entity_storage->expects($this->once())
      ->method('getFromStorage')
      ->with([$id])
      ->will($this->returnValue([$id => $entity]));

    $entities = $entity_storage->loadMultiple([$id]);
    $this->assertEquals($entity, $entities[$id]);
  }

  /**
   * @covers ::doLoadMultiple
   * @covers ::buildCacheId
   * @covers ::getFromPersistentCache
   * @covers ::setPersistentCache
   */
  public function testLoadMultiplePersistentCacheMiss() {
    $this->setUpModuleHandlerNoImplementations();

    $id = 1;
    $entity = $this->getMockBuilder('\Drupal\Tests\Core\Entity\Sql\SqlContentEntityStorageTestEntityInterface')
      ->getMockForAbstractClass();
    $entity->expects($this->any())
      ->method('id')
      ->will($this->returnValue($id));

    $this->entityType->expects($this->any())
      ->method('isPersistentlyCacheable')
      ->will($this->returnValue(TRUE));
    $this->entityType->expects($this->atLeastOnce())
      ->method('id')
      ->will($this->returnValue($this->entityTypeId));
    $this->entityType->expects($this->atLeastOnce())
      ->method('getClass')
      ->will($this->returnValue(get_class($entity)));

    // In case of a cache miss, the entity is loaded from the storage and then
    // set in the cache.
    $key = 'values:' . $this->entityTypeId . ':1';
    $this->cache->expects($this->once())
      ->method('getMultiple')
      ->with([$key])
      ->will($this->returnValue([]));
    $this->cache->expects($this->once())
      ->method('set')
      ->with($key, $entity, CacheBackendInterface::CACHE_PERMANENT, [$this->entityTypeId . '_values', 'entity_field_info']);

    $entity_storage = $this->getMockBuilder('Drupal\Core\Entity\Sql\SqlContentEntityStorage')
      ->setConstructorArgs([$this->entityType, $this->connection, $this->entityManager, $this->cache, $this->languageManager])
      ->setMethods(['getFromStorage', 'invokeStorageLoadHook'])
      ->getMock();
    $entity_storage->method('invokeStorageLoadHook')
      ->willReturn(NULL);
    $entity_storage->expects($this->once())
      ->method('getFromStorage')
      ->with([$id])
      ->will($this->returnValue([$id => $entity]));

    $entities = $entity_storage->loadMultiple([$id]);
    $this->assertEquals($entity, $entities[$id]);
  }

  /**
   * @covers ::hasData
   */
  public function testHasData() {
    $query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
    $query->expects(($this->once()))
      ->method('accessCheck')
      ->with(FALSE)
      ->willReturn($query);
    $query->expects(($this->once()))
      ->method('range')
      ->with(0, 1)
      ->willReturn($query);
    $query->expects(($this->once()))
      ->method('execute')
      ->willReturn([5]);

    $factory = $this->getMock(QueryFactoryInterface::class);
    $factory->expects($this->once())
      ->method('get')
      ->with($this->entityType, 'AND')
      ->willReturn($query);

    $this->container->set('entity.query.sql', $factory);

    $database = $this->getMockBuilder('Drupal\Core\Database\Connection')
      ->disableOriginalConstructor()
      ->getMock();

    $this->entityManager->expects($this->any())
      ->method('getDefinition')
      ->will($this->returnValue($this->entityType));

    $this->entityManager->expects($this->any())
      ->method('getFieldStorageDefinitions')
      ->will($this->returnValue($this->fieldDefinitions));

    $this->entityManager->expects($this->any())
      ->method('getBaseFieldDefinitions')
      ->will($this->returnValue($this->fieldDefinitions));

    $this->entityStorage = new SqlContentEntityStorage($this->entityType, $database, $this->entityManager, $this->cache, $this->languageManager);

    $result = $this->entityStorage->hasData();

    $this->assertTrue($result, 'hasData returned TRUE');
  }

  /**
   * Tests entity ID sanitization.
   */
  public function testCleanIds() {
    $valid_ids = [
      -1,
      0,
      1,
      '-1',
      '0',
      '1',
      0123,
      -0x1A,
      0x1AFC,
      -0b111,
      0b101,
      '0123',
      '00123',
      '000123',
      '-0123',
      '-00123',
      '-000123',
      -10.0,
      -1.0,
      0.0,
      1.0,
      10.0,
      -10.00,
      -1.00,
      0.00,
      1.00,
      10.00,
    ];

    $this->fieldDefinitions = $this->mockFieldDefinitions(['id']);
    $this->fieldDefinitions['id']->expects($this->any())
      ->method('getType')
      ->will($this->returnValue('integer'));

    $this->setUpEntityStorage();

    $this->entityType->expects($this->any())
      ->method('getKey')
      ->will($this->returnValueMap(
        [['id', 'id']]
      ));

    $method = new \ReflectionMethod($this->entityStorage, 'cleanIds');
    $method->setAccessible(TRUE);
    $this->assertEquals($valid_ids, $method->invoke($this->entityStorage, $valid_ids));

    $invalid_ids = [
      '--1',
      '-0x1A',
      '0x1AFC',
      '-0b111',
      '0b101',
      'a',
      FALSE,
      TRUE,
      NULL,
      '32acb',
      123.123,
      123.678,
    ];
    $this->assertEquals([], $method->invoke($this->entityStorage, $invalid_ids));

  }

  /**
   * Sets up the module handler with no implementations.
   */
  protected function setUpModuleHandlerNoImplementations() {
    $this->moduleHandler->expects($this->any())
      ->method('getImplementations')
      ->will($this->returnValueMap([
        ['entity_load', []],
        [$this->entityTypeId . '_load', []]
      ]));

    $this->container->set('module_handler', $this->moduleHandler);
  }

}

/**
 * Provides an entity with dummy implementations of static methods, because
 * those cannot be mocked.
 */
abstract class SqlContentEntityStorageTestEntityInterface implements EntityInterface {

  /**
   * {@inheritdoc}
   */
  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
  }

}
