<?php

namespace Drupal\Tests\dkan_datastore\Unit\Service;

use Drupal\dkan_common\DataResource;
use Drupal\Core\DependencyInjection\Container;
use Drupal\Core\Queue\QueueFactory;
use Drupal\dkan_metastore\ResourceMapper;
use Drupal\dkan_datastore\Storage\ImportJobStoreFactory;
use Drupal\Tests\dkan_datastore\Traits\TestHelperTrait;
use MockChain\Chain;
use MockChain\Options;
use Drupal\dkan_datastore\DatastoreService;
use Drupal\dkan_datastore\Service\Factory\ImportServiceFactory;
use Drupal\dkan_datastore\Service\ResourceLocalizer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\dkan_datastore\Service\DatastoreQuery;
use Drupal\dkan_datastore\Service\ImportService;
use Drupal\dkan_datastore\Service\Info\ImportInfoList;
use Drupal\dkan_datastore\Service\Query;
use Drupal\dkan_datastore\Storage\DatabaseTable;
use Drupal\dkan_datastore\Storage\QueryFactory;
use Drupal\dkan_metastore\Storage\Data;
use Drupal\dkan_metastore\Storage\DataFactory;
use Drupal\Tests\dkan_common\Unit\Storage\QueryDataProvider as QueryData;
use Drupal\dkan_datastore\Service\ResourceProcessor\DictionaryEnforcer;
use Drupal\dkan_common\Storage\AbstractJobStoreFactory;
use Drupal\dkan_metastore\Reference\ReferenceLookup;

/**
 * @group dkan
 * @group datastore
 * @group unit
 */
class DatastoreQueryTest extends TestCase {
  use TestHelperTrait;

  /**
   * @test
   *
   * @dataProvider queryCompareProvider()
   */
  public function testQueryCompare($testName) {
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson($testName);
    $storageMap = $queryService->getQueryStorageMap($datastoreQuery);
    $dkanQuery = QueryFactory::create($datastoreQuery, $storageMap);
    $dkanQueryCompare = QueryData::$testName(QueryData::QUERY_OBJECT);
    $dkanQueryCompare->showDbColumns = TRUE;
    $this->assertEquals(json_encode($dkanQuery, JSON_PRETTY_PRINT), json_encode($dkanQueryCompare, JSON_PRETTY_PRINT));
    $result = $queryService->runQuery($datastoreQuery);
    $this->assertIsArray($result->{"$.results"});
    $this->assertIsNumeric($result->{"$.count"});
    $this->assertIsArray($result->{"$.schema"});
    $this->assertIsArray($result->{"$.query"});
  }

  /**
   * Test a basic datastore query and response for expected properties.
   */
  public function testResultsQuery() {
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("propertiesQuery");
    $response = $queryService->runQuery($datastoreQuery);
    $this->assertIsArray($response->{"$.results[0]"});
    $this->assertEquals(123, $response->{"$.count"});
    $this->assertIsArray($response->{"$.schema"}["asdf"]["fields"]);
    $this->assertIsArray($response->{"$.query"});
  }

  /**
   * Test no keys behavior (array instead of keyed object).
   */
  public function testNoKeysQuery() {
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("propertiesQuery");
    $datastoreQuery->{"$.keys"} = FALSE;
    $response = $queryService->runQuery($datastoreQuery);
    $this->assertIsArray($response->{"$.results[0]"});
  }

  public function testRequiredCondition() {
    $this->expectExceptionMessage("JSON Schema validation failed.");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("badConditionQuery");
    $queryService->runQuery($datastoreQuery);
  }

  public function testInvalidConditionGroup() {
    $this->expectExceptionMessage("Invalid condition");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("invalidConditionGroupQuery");
    $queryService->runQuery($datastoreQuery);
  }

  public function testBadQueryProperty() {
    $this->expectExceptionMessage("JSON Schema validation failed.");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("badPropertyQuery");
    $queryService->runQuery($datastoreQuery);
  }

  public function testTooManyResourcesQuery() {
    $this->expectExceptionMessage("Too many resources specified.");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("tooManyResourcesQuery");
    $queryService->runQuery($datastoreQuery);
  }

  public function testInvalidQueryAgainstSchema() {
    $this->expectExceptionMessage("JSON Schema validation failed");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("invalidQuerySchema");
    $queryService->runQuery($datastoreQuery);
  }

  /**
   * Ensure if an ungrouped property is specified, the query fails.
   */
  public function testUngroupedPropertyInGroupByQueryFails(): void {
    $this->expectExceptionMessage('Un-grouped properties found in aggregate query: prop2');
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson('ungroupedGroupByQuery');
    $queryService->runQuery($datastoreQuery);
  }

  /**
   * Simple test for the stripRowKeys() method, now that it's public.
   */
  public function testStripRowKeys() {
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));

    $testRow = (object) [
      'field1' => 'foo',
      'field2' => 'bar',
    ];

    $result = $queryService->stripRowKeys($testRow);
    $this->assertEquals(['foo', 'bar'], $result);
  }

  /**
   * Ensure if an invalid limit is specified, the query fails.
   */
  public function testInvalidLimitQuery() {
    $this->expectExceptionMessage("JSON Schema validation failed");
    $container = $this->getCommonMockChain();
    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));
    $datastoreQuery = $this->getDatastoreQueryFromJson("invalidLimitQuery");
    $queryService->runQuery($datastoreQuery);
  }

  public function testRowIdsQuery() {
    $container = $this->getCommonMockChain()
      ->add(DatabaseTable::class, "getSchema", [
        "fields" => [
          "record_number" => 1,
          "a" => "a",
          "b" => "b",
          ],
        "primary key" => ["record_number"],
      ]);

    \Drupal::setContainer($container->getMock());
    $queryService = new Query(DatastoreService::create($container->getMock()));

    $datastoreQuery = $this->getDatastoreQueryFromJson('rowIdsQuery');
    $result = $queryService->runQuery($datastoreQuery);
    $this->assertEmpty($container->getStoredInput('DatabaseTableQuery')[0]->properties);
    $this->assertArrayHasKey('record_number', $result->{"$.schema"}["asdf"]["fields"]);

    $datastoreQuery = $this->getDatastoreQueryFromJson('defaultQuery');
    $result = $queryService->runQuery($datastoreQuery);
    $this->assertEquals(
      ["a", "b"],
      $container->getStoredInput('DatabaseTableQuery')[0]->properties
    );
    $this->assertArrayNotHasKey('record_number', $result->{"$.schema"}["asdf"]["fields"]);
  }

  /**
   * Data provider for query compare tests.
   */
  public static function queryCompareProvider() {
    return [
      ['propertiesQuery'],
      ['expressionQuery'],
      ['arrayConditionQuery'],
      ['likeConditionQuery'],
      ['nestedExpressionQuery'],
      ['nestedConditionGroupQuery'],
      ['sortQuery'],
      ['joinWithPropertiesFromBothQuery'],
      ['groupByQuery'],
    ];
  }

  private function getDatastoreQueryFromJson($payloadName): DatastoreQuery {
    $payload = file_get_contents(__DIR__ . "/../../../data/query/$payloadName.json");
    return new DatastoreQuery($payload);
  }

  /**
   * Build our mockChain.
   */
  public function getCommonMockChain() {

    $options = (new Options())
      ->add('event_dispatcher', EventDispatcherInterface::class)
      ->add('dkan.metastore.resource_mapper', ResourceMapper::class)
      ->add("dkan.datastore.query", Query::class)
      ->add("dkan.datastore.service", DatastoreService::class)
      ->add('dkan.datastore.service.resource_localizer', ResourceLocalizer::class)
      ->add("dkan.datastore.service.factory.import", ImportServiceFactory::class)
      ->add('queue', QueueFactory::class)
      ->add('request_stack', RequestStack::class)
      ->add('dkan.datastore.import_job_store_factory', ImportJobStoreFactory::class)
      ->add('dkan.metastore.storage', DataFactory::class)
      ->add('dkan.datastore.import_info_list', ImportInfoList::class)
      ->add('dkan.datastore.service.resource_processor.dictionary_enforcer', DictionaryEnforcer::class)
      ->add('dkan.metastore.reference_lookup', ReferenceLookup::class)
      ->index(0);

    $resource_metadata = '{"data":{"%Ref:downloadURL":[{"data":{"identifier":"qwerty","version":"uiop"}}]}}';
    $resource = new DataResource('http://example.org', 'text/csv');
    $queryResult = [(object) ["expression" => 123]];

    return (new Chain($this))
      ->add(Container::class, "get", $options)
      ->add(RequestStack::class, 'getCurrentRequest', Request::class)
      ->add(DataFactory::class, "getInstance", Data::class)
      ->add(Data::class, "retrieve", $resource_metadata)
      ->add(QueueFactory::class, "get", [])
      ->add(ResourceLocalizer::class, "get", $resource)
      ->add(ResourceMapper::class, 'get', $resource)
      ->add(ImportServiceFactory::class, "getInstance", ImportService::class)
      ->add(ImportService::class, "getStorage", DatabaseTable::class)
      ->add(DatabaseTable::class, "query", $queryResult, 'DatabaseTableQuery')
      ->add(DatabaseTable::class, "getSchema", ["fields" => ["a" => "a", "b" => "b"]])
      ->add(DatabaseTable::class, "getTableName", "table2")
      ->add(DatabaseTable::class, "primaryKey", "record_number")
      ->add(ReferenceLookup::class, 'getReferencers', [$resource->getIdentifier()])
      ->add(ReferenceLookup::class, 'invalidateReferencerCacheTags');
  }

}
