<?php

declare(strict_types=1);

namespace Tests\Endpoints;

use MeiliSearch\Exceptions\ApiException;
use Tests\TestCase;

final class SearchTest extends TestCase
{
    private $index;

    protected function setUp(): void
    {
        parent::setUp();
        $this->index = $this->client->createIndex('index');
        $promise = $this->index->updateDocuments(self::DOCUMENTS);
        $this->index->waitForPendingUpdate($promise['updateId']);
    }

    public function testBasicSearch(): void
    {
        $response = $this->index->search('prince');

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertSame(2, $response->getNbHits());
        $this->assertCount(2, $response->getHits());

        $response = $this->index->search('prince', [], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(2, $response['nbHits']);
    }

    public function testBasicEmptySearch(): void
    {
        $response = $this->index->search('');

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertCount(7, $response->getHits());

        $response = $this->index->search('', [], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(7, $response['nbHits']);
    }

    public function testBasicPlaceholderSearch(): void
    {
        $response = $this->index->search(null);

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertCount(\count(self::DOCUMENTS), $response->getHits());

        $response = $this->index->search(null, [], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(\count(self::DOCUMENTS), $response['nbHits']);
    }

    public function testSearchWithOptions(): void
    {
        $response = $this->index->search('prince', ['limit' => 1]);

        $this->assertCount(1, $response->getHits());

        $response = $this->index->search('prince', ['limit' => 1], [
            'raw' => true,
        ]);

        $this->assertSame(1, \count($response['hits']));
    }

    public function testBasicSearchIfNoPrimaryKeyAndDocumentProvided(): void
    {
        $emptyIndex = $this->client->createIndex('empty');

        $res = $emptyIndex->search('prince');

        $this->assertArrayHasKey('hits', $res->toArray());
        $this->assertArrayHasKey('offset', $res->toArray());
        $this->assertArrayHasKey('limit', $res->toArray());
        $this->assertArrayHasKey('processingTimeMs', $res->toArray());
        $this->assertArrayHasKey('query', $res->toArray());
        $this->assertCount(0, $res->getHits());

        $res = $emptyIndex->search('prince', [], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('hits', $res);
        $this->assertArrayHasKey('offset', $res);
        $this->assertArrayHasKey('limit', $res);
        $this->assertArrayHasKey('processingTimeMs', $res);
        $this->assertArrayHasKey('query', $res);
        $this->assertSame(0, $res['nbHits']);
    }

    public function testExceptionIfNoIndexWhenSearching(): void
    {
        $index = $this->client->createIndex('another-index');
        $index->delete();

        $this->expectException(ApiException::class);

        $index->search('prince');
    }

    public function testParametersArray(): void
    {
        $response = $this->index->search('prince', [
            'limit' => 5,
            'offset' => 0,
            'attributesToRetrieve' => ['id', 'title'],
            'attributesToCrop' => ['id', 'title'],
            'cropLength' => 6,
            'attributesToHighlight' => ['title'],
            'filters' => 'title = "Le Petit Prince"',
            'matches' => true,
        ]);

        $this->assertArrayHasKey('_matchesInfo', $response->getHit(0));
        $this->assertArrayHasKey('title', $response->getHit(0)['_matchesInfo']);
        $this->assertArrayHasKey('_formatted', $response->getHit(0));
        $this->assertArrayNotHasKey('comment', $response->getHit(0));
        $this->assertArrayNotHasKey('comment', $response->getHit(0)['_matchesInfo']);
        $this->assertSame('Petit <em>Prince</em>', $response->getHit(0)['_formatted']['title']);

        $response = $this->index->search('prince', [
            'limit' => 5,
            'offset' => 0,
            'attributesToRetrieve' => ['id', 'title'],
            'attributesToCrop' => ['id', 'title'],
            'cropLength' => 6,
            'attributesToHighlight' => ['title'],
            'filters' => 'title = "Le Petit Prince"',
            'matches' => true,
        ], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('_matchesInfo', $response['hits'][0]);
        $this->assertArrayHasKey('title', $response['hits'][0]['_matchesInfo']);
        $this->assertArrayHasKey('_formatted', $response['hits'][0]);
        $this->assertArrayNotHasKey('comment', $response['hits'][0]);
        $this->assertArrayNotHasKey('comment', $response['hits'][0]['_matchesInfo']);
        $this->assertSame('Petit <em>Prince</em>', $response['hits'][0]['_formatted']['title']);
    }

    public function testParametersCanBeAStar(): void
    {
        $response = $this->index->search('prince', [
            'limit' => 5,
            'offset' => 0,
            'attributesToRetrieve' => ['*'],
            'attributesToCrop' => ['*'],
            'cropLength' => 6,
            'attributesToHighlight' => ['*'],
            'filters' => 'title = "Le Petit Prince"',
            'matches' => true,
        ]);

        $this->assertArrayHasKey('_matchesInfo', $response->getHit(0));
        $this->assertArrayHasKey('title', $response->getHit(0)['_matchesInfo']);
        $this->assertArrayHasKey('_formatted', $response->getHit(0));
        $this->assertArrayHasKey('comment', $response->getHit(0));
        $this->assertArrayNotHasKey('comment', $response->getHit(0)['_matchesInfo']);
        $this->assertSame('Petit <em>Prince</em>', $response->getHit(0)['_formatted']['title']);

        $response = $this->index->search('prince', [
            'limit' => 5,
            'offset' => 0,
            'attributesToRetrieve' => ['*'],
            'attributesToCrop' => ['*'],
            'cropLength' => 6,
            'attributesToHighlight' => ['*'],
            'filters' => 'title = "Le Petit Prince"',
            'matches' => true,
        ], [
            'raw' => true,
        ]);

        $this->assertArrayHasKey('_matchesInfo', $response['hits'][0]);
        $this->assertArrayHasKey('title', $response['hits'][0]['_matchesInfo']);
        $this->assertArrayHasKey('_formatted', $response['hits'][0]);
        $this->assertArrayHasKey('comment', $response['hits'][0]);
        $this->assertArrayNotHasKey('comment', $response['hits'][0]['_matchesInfo']);
        $this->assertSame('Petit <em>Prince</em>', $response['hits'][0]['_formatted']['title']);
    }

    public function testBasicSearchWithFacetsDistribution(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search('prince', [
            'facetsDistribution' => ['genre'],
        ]);
        $this->assertSame(2, $response->getHitsCount());
        $this->assertArrayHasKey('facetsDistribution', $response->toArray());
        $this->assertArrayHasKey('exhaustiveFacetsCount', $response->toArray());
        $this->assertArrayHasKey('genre', $response->getFacetsDistribution());
        $this->assertTrue($response->getExhaustiveFacetsCount());
        $this->assertSame($response->getFacetsDistribution()['genre']['fantasy'], 1);
        $this->assertSame($response->getFacetsDistribution()['genre']['adventure'], 1);
        $this->assertSame($response->getFacetsDistribution()['genre']['romance'], 0);

        $response = $this->index->search('prince', [
            'facetsDistribution' => ['genre'],
        ], [
            'raw' => true,
        ]);
        $this->assertSame(2, $response['nbHits']);
        $this->assertArrayHasKey('facetsDistribution', $response);
        $this->assertArrayHasKey('exhaustiveFacetsCount', $response);
        $this->assertArrayHasKey('genre', $response['facetsDistribution']);
        $this->assertTrue($response['exhaustiveFacetsCount']);
        $this->assertSame($response['facetsDistribution']['genre']['fantasy'], 1);
        $this->assertSame($response['facetsDistribution']['genre']['adventure'], 1);
        $this->assertSame($response['facetsDistribution']['genre']['romance'], 0);
    }

    public function testBasicSearchWithFacetFilters(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search('prince', [
            'facetFilters' => [['genre:fantasy']],
        ]);
        $this->assertSame(1, $response->getHitsCount());
        $this->assertArrayNotHasKey('facetsDistribution', $response->getRaw());
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response->getRaw());
        $this->assertSame(4, $response->getHit(0)['id']);

        $response = $this->index->search('prince', [
            'facetFilters' => [['genre:fantasy']],
        ], [
            'raw' => true,
        ]);
        $this->assertSame(1, $response['nbHits']);
        $this->assertArrayNotHasKey('facetsDistribution', $response);
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response);
        $this->assertSame(4, $response['hits'][0]['id']);
    }

    public function testBasicSearchWithMultipleFacetFilters(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search('prince', [
            'facetFilters' => ['genre:fantasy', ['genre:fantasy', 'genre:fantasy']],
        ]);
        $this->assertSame(1, $response->getHitsCount());
        $this->assertArrayNotHasKey('facetsDistribution', $response->getRaw());
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response->getRaw());
        $this->assertSame(4, $response->getHit(0)['id']);

        $response = $this->index->search('prince', [
            'facetFilters' => ['genre:fantasy', ['genre:fantasy', 'genre:fantasy']],
        ], [
            'raw' => true,
        ]);
        $this->assertSame(1, $response['nbHits']);
        $this->assertArrayNotHasKey('facetsDistribution', $response);
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response);
        $this->assertSame(4, $response['hits'][0]['id']);
    }

    public function testCustomSearchWithFacetFiltersAndAttributesToRetrieve(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search('prince', [
            'facetFilters' => [['genre:fantasy']],
            'attributesToRetrieve' => ['id', 'title'],
        ]);
        $this->assertSame(1, $response->getHitsCount());
        $this->assertArrayNotHasKey('facetsDistribution', $response->getRaw());
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response->getRaw());
        $this->assertSame(4, $response->getHit(0)['id']);
        $this->assertArrayHasKey('id', $response->getHit(0));
        $this->assertArrayHasKey('title', $response->getHit(0));
        $this->assertArrayNotHasKey('comment', $response->getHit(0));

        $response = $this->index->search('prince', [
            'facetFilters' => [['genre:fantasy']],
            'attributesToRetrieve' => ['id', 'title'],
        ], [
            'raw' => true,
        ]);
        $this->assertSame(1, $response['nbHits']);
        $this->assertArrayNotHasKey('facetsDistribution', $response);
        $this->assertArrayNotHasKey('exhaustiveFacetsCount', $response);
        $this->assertSame(4, $response['hits'][0]['id']);
        $this->assertArrayHasKey('id', $response['hits'][0]);
        $this->assertArrayHasKey('title', $response['hits'][0]);
        $this->assertArrayNotHasKey('comment', $response['hits'][0]);
    }

    public function testBasicSerachWithRawSearch(): void
    {
        $response = $this->index->rawSearch('prince');

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(2, $response['nbHits']);
        $this->assertCount(2, $response['hits']);
        $this->assertEquals('Le Petit Prince', $response['hits'][0]['title']);
    }

    public function testBasicSearchWithRawOption(): void
    {
        $response = $this->index->search('prince', [], ['raw' => true]);

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(2, $response['nbHits']);
        $this->assertCount(2, $response['hits']);
    }

    public function testBasicSearchWithTransformHitsOptionToFilter(): void
    {
        $keepLePetitPrinceFunc = function (array $hits) {
            return array_filter(
                $hits,
                function (array $hit) { return 'Le Petit Prince' === $hit['title']; }
            );
        };

        $response = $this->index->search('prince', [], $options = ['transformHits' => $keepLePetitPrinceFunc]);

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertSame('Le Petit Prince', $response->getHit(0)['title']);
        $this->assertSame(2, $response->getNbHits());
        $this->assertSame(1, $response->getHitsCount());
        $this->assertSame(1, $response->count());
    }

    public function testBasicSearchWithTransformHitsOptionToMap(): void
    {
        $titlesToUpperCaseFunc = function (array $hits) {
            return array_map(
                function (array $hit) {
                    $hit['title'] = strtoupper($hit['title']);

                    return $hit;
                },
                $hits
            );
        };

        $response = $this->index->search('prince', [], ['transformHits' => $titlesToUpperCaseFunc]);

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertSame(2, $response->getNbHits());
        $this->assertSame(2, $response->getHitsCount());
        $this->assertCount(2, $response->getHits());
        $this->assertSame('LE PETIT PRINCE', $response->getHits()[0]['title']);
    }

    public function testBasicSearchCannotBeFilteredOnRawResult(): void
    {
        $keepLePetitPrinceFunc = function (array $hits) {
            return array_filter(
                $hits,
                function (array $hit) { return 'Le Petit Prince' === $hit['title']; }
            );
        };

        $response = $this->index->search('prince', [], [
            'raw' => true,
            'transformHits' => $keepLePetitPrinceFunc,
        ]);

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(2, $response['nbHits']);
        $this->assertCount(2, $response['hits']);
    }

    public function testBasicSearchCanBeFilteredOnRawResultIfUsingToArray(): void
    {
        $keepLePetitPrinceFunc = function (array $hits) {
            return array_filter(
                $hits,
                function (array $hit) { return 'Le Petit Prince' === $hit['title']; }
            );
        };

        $response = $this->index->search('prince', [], ['transformHits' => $keepLePetitPrinceFunc])->toArray();

        $this->assertArrayHasKey('hits', $response);
        $this->assertArrayHasKey('offset', $response);
        $this->assertArrayHasKey('limit', $response);
        $this->assertArrayHasKey('processingTimeMs', $response);
        $this->assertArrayHasKey('query', $response);
        $this->assertSame(2, $response['nbHits']);
        $this->assertCount(1, $response['hits']);
        $this->assertEquals('Le Petit Prince', $response['hits'][0]['title']);
    }

    public function testBasicSearchWithRemoveZeroFacetsOption(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search(
            'prince',
            ['facetsDistribution' => ['genre']],
            ['removeZeroFacets' => true]
        );

        $this->assertCount(2, $response->getFacetsDistribution()['genre']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['adventure']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['fantasy']);
        $this->assertCount(3, $response->getRaw()['facetsDistribution']['genre']);
        $this->assertEquals($response->getRaw()['hits'], $response->getHits());
        $this->assertNotEquals($response->getRaw()['facetsDistribution'], $response->getFacetsDistribution());
    }

    public function testBasicSearchWithRemoveZeroFacetsOptionAndMultipleFacets(): void
    {
        $response = $this->index->addDocuments([['id' => 32, 'title' => 'The Witcher', 'genre' => 'adventure', 'adaptation' => 'video game']]);
        $this->index->waitForPendingUpdate($response['updateId']);
        $response = $this->index->updateAttributesForFaceting(['genre', 'adaptation']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $response = $this->index->search(
            'prince',
            ['facetsDistribution' => ['genre', 'adaptation']],
            ['removeZeroFacets' => true]
        );

        $this->assertCount(2, $response->getFacetsDistribution()['genre']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['adventure']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['fantasy']);
        $this->assertEquals([], $response->getFacetsDistribution()['adaptation']);
        $this->assertCount(1, $response->getRaw()['facetsDistribution']['adaptation']);
        $this->assertCount(3, $response->getRaw()['facetsDistribution']['genre']);
        $this->assertEquals($response->getRaw()['hits'], $response->getHits());
        $this->assertNotEquals($response->getRaw()['facetsDistribution'], $response->getFacetsDistribution());
    }

    public function testBasicSearchWithTransformFacetsDritributionOptionToFilter(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $filterAllFacets = function (array $facets) {
            $filterOneFacet = function (array $facet) {
                return array_filter(
                    $facet,
                    function ($v, $k) { return $v > 1; },
                    ARRAY_FILTER_USE_BOTH
                );
            };

            return array_map($filterOneFacet, $facets);
        };

        $response = $this->index->search(
            null,
            ['facetsDistribution' => ['genre']],
            ['transformFacetsDistribution' => $filterAllFacets]
        );

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('facetsDistribution', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertEquals($response->getRaw()['hits'], $response->getHits());
        $this->assertNotEquals($response->getRaw()['facetsDistribution'], $response->getFacetsDistribution());
        $this->assertCount(3, $response->getRaw()['facetsDistribution']['genre']);
        $this->assertCount(2, $response->getFacetsDistribution()['genre']);
        $this->assertEquals(3, $response->getFacetsDistribution()['genre']['romance']);
        $this->assertEquals(2, $response->getFacetsDistribution()['genre']['fantasy']);
    }

    public function testBasicSearchWithTransformFacetsDritributionOptionToMap(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $facetsToUpperFunc = function (array $facets) {
            $changeOneFacet = function (array $facet) {
                $result = [];
                foreach ($facet as $k => $v) {
                    $result[strtoupper($k)] = $v;
                }

                return $result;
            };

            return array_map($changeOneFacet, $facets);
        };

        $response = $this->index->search(
            null,
            ['facetsDistribution' => ['genre']],
            ['transformFacetsDistribution' => $facetsToUpperFunc]
        );

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('facetsDistribution', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertEquals($response->getRaw()['hits'], $response->getHits());
        $this->assertNotEquals($response->getRaw()['facetsDistribution'], $response->getFacetsDistribution());
        $this->assertCount(3, $response->getFacetsDistribution()['genre']);
        $this->assertEquals(3, $response->getFacetsDistribution()['genre']['ROMANCE']);
        $this->assertEquals(2, $response->getFacetsDistribution()['genre']['FANTASY']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['ADVENTURE']);
    }

    public function testBasicSearchWithTransformFacetsDritributionOptionToOder(): void
    {
        $response = $this->index->updateAttributesForFaceting(['genre']);
        $this->index->waitForPendingUpdate($response['updateId']);

        $facetsToUpperFunc = function (array $facets) {
            $sortOneFacet = function (array $facet) {
                ksort($facet);

                return $facet;
            };

            return array_map($sortOneFacet, $facets);
        };

        $response = $this->index->search(
            null,
            ['facetsDistribution' => ['genre']],
            ['transformFacetsDistribution' => $facetsToUpperFunc]
        );

        $this->assertArrayHasKey('hits', $response->toArray());
        $this->assertArrayHasKey('facetsDistribution', $response->toArray());
        $this->assertArrayHasKey('offset', $response->toArray());
        $this->assertArrayHasKey('limit', $response->toArray());
        $this->assertArrayHasKey('processingTimeMs', $response->toArray());
        $this->assertArrayHasKey('query', $response->toArray());
        $this->assertEquals($response->getRaw()['hits'], $response->getHits());
        $this->assertEquals('adventure', array_key_first($response->getFacetsDistribution()['genre']));
        $this->assertEquals('romance', array_key_last($response->getFacetsDistribution()['genre']));
        $this->assertCount(3, $response->getFacetsDistribution()['genre']);
        $this->assertEquals(3, $response->getFacetsDistribution()['genre']['romance']);
        $this->assertEquals(2, $response->getFacetsDistribution()['genre']['fantasy']);
        $this->assertEquals(1, $response->getFacetsDistribution()['genre']['adventure']);
    }
}
