<?php

namespace Drupal\Tests\search_api\Unit\Processor;

use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\DataType\DataTypeInterface;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Plugin\search_api\processor\AggregatedFields;
use Drupal\search_api\Plugin\search_api\processor\Property\AggregatedFieldProperty;
use Drupal\search_api\Processor\ProcessorInterface;
use Drupal\search_api\Processor\ProcessorProperty;
use Drupal\search_api\Utility\PluginHelperInterface;
use Drupal\search_api\Utility\Utility;
use Drupal\Tests\search_api\Unit\TestComplexDataInterface;
use Drupal\Tests\UnitTestCase;

/**
 * Tests the "Aggregated fields" processor.
 *
 * @group search_api
 *
 * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedFields
 */
class AggregatedFieldsTest extends UnitTestCase {

  use TestItemsTrait;

  /**
   * The processor to be tested.
   *
   * @var \Drupal\search_api\Plugin\search_api\processor\AggregatedFields
   */
  protected $processor;

  /**
   * A search index mock for the tests.
   *
   * @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject
   */
  protected $index;

  /**
   * The field ID used in this test.
   *
   * @var string
   */
  protected $fieldId = 'aggregated_field';

  /**
   * The callback with which text values should be preprocessed.
   *
   * @var callable
   */
  protected $valueCallback;

  /**
   * Creates a new processor object for use in the tests.
   */
  protected function setUp() {
    parent::setUp();

    $datasource = $this->getMock(DatasourceInterface::class);
    $datasource->expects($this->any())
      ->method('getPropertyDefinitions')
      ->willReturn([]);
    $this->index = new Index([
      'datasourceInstances' => [
        'entity:test1' => $datasource,
        'entity:test2' => $datasource,
      ],
      'processorInstances' => [],
      'field_settings' => [
        'foo' => [
          'type' => 'string',
          'datasource_id' => 'entity:test1',
          'property_path' => 'foo',
        ],
        'bar' => [
          'type' => 'string',
          'datasource_id' => 'entity:test1',
          'property_path' => 'foo:bar',
        ],
        'bla' => [
          'type' => 'string',
          'datasource_id' => 'entity:test2',
          'property_path' => 'foobaz:bla',
        ],
        'aggregated_field' => [
          'type' => 'text',
          'property_path' => 'aggregated_field',
        ],
      ],
    ], 'search_api_index');
    $this->processor = new AggregatedFields(['#index' => $this->index], 'aggregated_field', []);
    $this->index->addProcessor($this->processor);
    $this->setUpMockContainer();

    $plugin_helper = $this->getMockBuilder(PluginHelperInterface::class)
      ->disableOriginalConstructor()
      ->getMock();
    $plugin_helper->method('getProcessorsByStage')
      ->willReturn([]);
    $this->container->set('search_api.plugin_helper', $plugin_helper);

    // We want to check correct data type handling, so we need a somewhat more
    // complex mock-up for the datatype plugin handler.
    /** @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\search_api\DataType\DataTypePluginManager $data_type_manager */
    $data_type_manager = $this->container->get('plugin.manager.search_api.data_type');
    $data_type_manager->method('hasDefinition')
      ->willReturn(TRUE);
    $this->valueCallback = function ($value) {
      if (is_numeric($value)) {
        return $value + 1;
      }
      else {
        return '*' . $value;
      }
    };
    $data_type = $this->getMock(DataTypeInterface::class);
    $data_type->method('getValue')
      ->willReturnCallback($this->valueCallback);
    $data_type_manager->method('createInstance')
      ->willReturnMap([
        ['text', [], $data_type],
      ]);
  }

  /**
   * Tests aggregated fields of the given type.
   *
   * @param string $type
   *   The aggregation type to test.
   * @param array $expected
   *   The expected values for the two items.
   * @param bool $integer
   *   (optional) TRUE if the items' normal fields should contain integers,
   *   FALSE otherwise.
   *
   * @dataProvider aggregationTestsDataProvider
   */
  public function testAggregation($type, array $expected, $integer = FALSE) {
    // Add the field configuration.
    $configuration = [
      'type' => $type,
      'fields' => [
        'entity:test1/foo',
        'entity:test1/foo:bar',
        'entity:test2/foobaz:bla',
      ],
    ];
    $this->index->getField($this->fieldId)->setConfiguration($configuration);

    if ($integer) {
      $field_values = [
        'foo' => [2, 4],
        'bar' => [16],
        'bla' => [7],
      ];
    }
    else {
      $field_values = [
        'foo' => ['foo', 'bar'],
        'bar' => ['baz'],
        'bla' => ['foobar'],
      ];
    }
    $items = [];
    $i = 0;
    foreach (['entity:test1', 'entity:test2'] as $datasource_id) {
      $this->itemIds[$i++] = $item_id = Utility::createCombinedId($datasource_id, '1:en');
      $item = \Drupal::getContainer()
        ->get('search_api.fields_helper')
        ->createItem($this->index, $item_id);
      foreach ([NULL, $datasource_id] as $field_datasource_id) {
        foreach ($this->index->getFieldsByDatasource($field_datasource_id) as $field_id => $field) {
          $field = clone $field;
          if (!empty($field_values[$field_id])) {
            $field->setValues($field_values[$field_id]);
          }
          $item->setField($field_id, $field);
        }
      }
      $item->setFieldsExtracted(TRUE);
      $items[$item_id] = $item;
    }

    // Add the processor's field values to the items.
    foreach ($items as $item) {
      $this->processor->addFieldValues($item);
    }

    $this->assertEquals(array_map($this->valueCallback, $expected[0]), $items[$this->itemIds[0]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 1.');
    $this->assertEquals(array_map($this->valueCallback, $expected[1]), $items[$this->itemIds[1]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 2.');
  }

  /**
   * Provides test data for aggregation tests.
   *
   * @return array
   *   An array containing test data sets, with each being an array of
   *   arguments to pass to the test method.
   *
   * @see static::testAggregation()
   */
  public function aggregationTestsDataProvider() {
    return [
      '"Union" aggregation' => [
        'union',
        [
          ['foo', 'bar', 'baz'],
          ['foobar'],
        ],
      ],
      '"Concatenation" aggregation' => [
        'concat',
        [
          ["foo\n\nbar\n\nbaz"],
          ['foobar'],
        ],
      ],
      '"Sum" aggregation' => [
        'sum',
        [
          [22],
          [7],
        ],
        TRUE,
      ],
      '"Count" aggregation' => [
        'count',
        [
          [3],
          [1],
        ],
      ],
      '"Maximum" aggregation' => [
        'max',
        [
          [16],
          [7],
        ],
        TRUE,
      ],
      '"Minimum" aggregation' => [
        'min',
        [
          [2],
          [7],
        ],
        TRUE,
      ],
      '"First" aggregation' => [
        'first',
        [
          ['foo'],
          ['foobar'],
        ],
      ],
      '"Last" aggregation' => [
        'last',
        [
          ['baz'],
          ['foobar'],
        ],
      ],
    ];
  }

  /**
   * Tests whether the properties are correctly altered.
   *
   * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedFields::getPropertyDefinitions()
   */
  public function testGetPropertyDefinitions() {
    /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
    $translation = $this->getStringTranslationStub();
    $this->processor->setStringTranslation($translation);

    // Check for added properties when no datasource is given.
    /** @var \Drupal\search_api\Processor\ProcessorPropertyInterface[] $properties */
    $properties = $this->processor->getPropertyDefinitions(NULL);

    $this->assertArrayHasKey('aggregated_field', $properties, 'The "aggregated_field" property was added to the properties.');
    $this->assertInstanceOf(AggregatedFieldProperty::class, $properties['aggregated_field'], 'The "aggregated_field" property has the correct class.');
    $this->assertEquals('string', $properties['aggregated_field']->getDataType(), 'Correct data type set in the data definition.');
    $this->assertEquals($translation->translate('Aggregated field'), $properties['aggregated_field']->getLabel(), 'Correct label set in the data definition.');
    $expected_description = $translation->translate('An aggregation of multiple other fields.');
    $this->assertEquals($expected_description, $properties['aggregated_field']->getDescription(), 'Correct description set in the data definition.');

    // Verify that there are no properties if a datasource is given.
    $datasource = $this->getMock(DatasourceInterface::class);
    $properties = $this->processor->getPropertyDefinitions($datasource);
    $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
  }

  /**
   * Tests that field extraction in the processor works correctly.
   */
  public function testFieldExtraction() {
    /** @var \Drupal\Tests\search_api\Unit\TestComplexDataInterface|\PHPUnit_Framework_MockObject_MockObject $object */
    $object = $this->getMock(TestComplexDataInterface::class);
    $bar_foo_property = $this->getMock(TypedDataInterface::class);
    $bar_foo_property->method('getValue')
      ->willReturn('value3');
    $bar_foo_property->method('getDataDefinition')
      ->willReturn(new DataDefinition());
    $bar_property = $this->getMock(TestComplexDataInterface::class);
    $bar_property->method('get')
      ->willReturnMap([
        ['foo', $bar_foo_property],
      ]);
    $bar_property->method('getProperties')
      ->willReturn([
        'foo' => TRUE,
      ]);
    $foobar_property = $this->getMock(TypedDataInterface::class);
    $foobar_property->method('getValue')
      ->willReturn('wrong_value2');
    $foobar_property->method('getDataDefinition')
      ->willReturn(new DataDefinition());
    $object->method('get')
      ->willReturnMap([
        ['bar', $bar_property],
        ['foobar', $foobar_property],
      ]);
    $object->method('getProperties')
      ->willReturn([
        'bar' => TRUE,
        'foobar' => TRUE,
      ]);

    /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */
    $index = $this->getMock(IndexInterface::class);

    $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
    $field = $fields_helper->createField($index, 'aggregated_field', [
      'property_path' => 'aggregated_field',
      'configuration' => [
        'type' => 'union',
        'fields' => [
          'aggregated_field',
          'foo',
          'entity:test1/bar:foo',
          'entity:test1/baz',
          'entity:test2/foobar',
        ],
      ],
    ]);
    $index->method('getFields')->willReturn([
      'aggregated_field' => $field,
    ]);
    $index->method('getPropertyDefinitions')
      ->willReturnMap([
        [
          NULL,
          [
            'foo' => new ProcessorProperty([
              'processor_id' => 'processor1',
            ]),
          ],
        ],
        [
          'entity:test1',
          [
            'bar' => new DataDefinition(),
            'foobar' => new DataDefinition(),
          ],
        ],
      ]);
    $processor_mock = $this->getMock(ProcessorInterface::class);
    $processor_mock->method('addFieldValues')
      ->willReturnCallback(function (ItemInterface $item) {
        foreach ($item->getFields(FALSE) as $field) {
          if ($field->getCombinedPropertyPath() == 'foo') {
            $field->setValues(['value4', 'value5']);
          }
        }
      });
    $index->method('getProcessorsByStage')
      ->willReturnMap([
        [
          ProcessorInterface::STAGE_ADD_PROPERTIES,
          [],
          [
            'aggregated_field' => $this->processor,
            'processor1' => $processor_mock,
          ],
        ],
      ]);
    $this->processor->setIndex($index);

    /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit_Framework_MockObject_MockObject $datasource */
    $datasource = $this->getMock(DatasourceInterface::class);
    $datasource->method('getPluginId')
      ->willReturn('entity:test1');

    $item = $fields_helper->createItem($index, 'id', $datasource);
    $item->setOriginalObject($object);
    $item->setField('aggregated_field', clone $field);
    $item->setField('test1', $fields_helper->createField($index, 'test1', [
      'property_path' => 'baz',
      'values' => [
        'wrong_value1',
      ],
    ]));
    $item->setField('test2', $fields_helper->createField($index, 'test2', [
      'datasource_id' => 'entity:test1',
      'property_path' => 'baz',
      'values' => [
        'value1',
        'value2',
      ],
    ]));
    $item->setFieldsExtracted(TRUE);

    $this->processor->addFieldValues($item);

    $expected = [
      'value1',
      'value2',
      'value3',
      'value4',
      'value5',
    ];
    $actual = $item->getField('aggregated_field')->getValues();
    sort($actual);
    $this->assertEquals($expected, $actual);
  }

}
