<?php

namespace Tests\Unit;

use App\Services\Search\Drivers\ManticoreSearchDriver;
use App\Services\Search\DTO\ReleaseSearchQuery;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use ReflectionClass;

class ManticoreSearchQueryTest extends TestCase
{
    #[Test]
    #[DataProvider('negationQueriesProvider')]
    public function it_preserves_negation_operators(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function negationQueriesProvider(): array
    {
        return [
            'negation with !' => ['!circus', '!circus'],
            'negation with -' => ['-circus', '-circus'],
            'word + negation' => ['dead !circus', 'dead !circus'],
            'word + hyphen negation' => ['dead -circus', 'dead -circus'],
            'multiple negations' => ['!foo !bar', '!foo !bar'],
        ];
    }

    #[Test]
    #[DataProvider('phraseQueriesProvider')]
    public function it_preserves_phrase_search(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function phraseQueriesProvider(): array
    {
        return [
            'exact phrase' => ['"exact phrase"', '"exact phrase"'],
            'negated phrase with !' => ['!"exact phrase"', '!"exact phrase"'],
            'negated phrase with -' => ['-"exact phrase"', '-"exact phrase"'],
            'word + phrase' => ['hello "world peace"', 'hello "world peace"'],
        ];
    }

    #[Test]
    #[DataProvider('orQueriesProvider')]
    public function it_preserves_or_operator(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function orQueriesProvider(): array
    {
        return [
            'basic OR' => ['cats | dogs', 'cats | dogs'],
            'OR with negation' => ['cats | -dogs', 'cats | -dogs'],
        ];
    }

    #[Test]
    #[DataProvider('wildcardQueriesProvider')]
    public function it_preserves_wildcards(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function wildcardQueriesProvider(): array
    {
        return [
            'suffix wildcard' => ['test*', 'test*'],
            'prefix wildcard' => ['*fix', '*fix'],
            'both wildcards' => ['*mid*', '*mid*'],
        ];
    }

    #[Test]
    #[DataProvider('groupingQueriesProvider')]
    public function it_preserves_grouping_parens(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function groupingQueriesProvider(): array
    {
        return [
            'basic grouping' => ['(cats | dogs)', '(cats | dogs)'],
            'grouping with negation' => ['(cats | dogs) -birds', '(cats | dogs) -birds'],
        ];
    }

    #[Test]
    #[DataProvider('escapingQueriesProvider')]
    public function it_still_escapes_dangerous_characters(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{string, string}> */
    public static function escapingQueriesProvider(): array
    {
        return [
            'escapes @' => ['@field test', '\@field test'],
            'escapes ~' => ['test~2', 'test\~2'],
            'escapes $' => ['test$', 'test\$'],
            'mid-word hyphen escaped' => ['spider-man', 'spider\-man'],
            'mid-word ! escaped' => ['wow!great', 'wow\!great'],
        ];
    }

    #[Test]
    #[DataProvider('edgeCaseQueriesProvider')]
    public function it_handles_edge_cases(string $input, string $expected): void
    {
        $result = ManticoreSearchDriver::prepareUserSearchQuery($input);
        $this->assertSame($expected, $result);
    }

    #[Test]
    public function it_scopes_multi_word_queries_to_the_requested_field(): void
    {
        $reflection = new ReflectionClass(ManticoreSearchDriver::class);
        $method = $reflection->getMethod('scopePreparedQueryToField');

        $this->assertSame(
            '@searchname (Resurrection 2025)',
            $method->invoke(null, 'Resurrection 2025', '@searchname')
        );
        $this->assertSame(
            '@(searchname,plainsearchname) (Resurrection 2025)',
            $method->invoke(null, 'Resurrection 2025', '@(searchname,plainsearchname)')
        );
        $this->assertSame(
            '(Resurrection 2025)',
            $method->invoke(null, 'Resurrection 2025')
        );
    }

    #[Test]
    public function it_adds_a_normalized_release_name_query_for_punctuation_separators(): void
    {
        $reflection = new ReflectionClass(ManticoreSearchDriver::class);
        $method = $reflection->getMethod('scopeReleaseSearchQuery');

        $query = $method->invoke(
            null,
            'Love.Is.A.Dogs.Best.Friend.2025.1080p.WEB-DL.HEVC.x265-BONE',
            ManticoreSearchDriver::prepareUserSearchQuery('Love.Is.A.Dogs.Best.Friend.2025.1080p.WEB-DL.HEVC.x265-BONE'),
            '@searchname'
        );

        $this->assertStringContainsString('@searchname (Love.Is.A.Dogs.Best.Friend.2025.1080p.WEB\\-DL.HEVC.x265\\-BONE)', $query);
        $this->assertStringContainsString('@searchname (Love Is A Dogs Best Friend 2025 1080p WEB DL HEVC x265 BONE)', $query);
    }

    /** @return array<string, array{string, string}> */
    public static function edgeCaseQueriesProvider(): array
    {
        return [
            'empty string' => ['', ''],
            'only whitespace' => ['   ', ''],
            'just asterisk' => ['*', ''],
            'plain word' => ['circus', 'circus'],
            'multiple words' => ['hello world', 'hello world'],
        ];
    }

    #[Test]
    public function escape_string_still_escapes_everything(): void
    {
        // Verify the original escapeString still escapes operators (for non-search use cases)
        $result = ManticoreSearchDriver::escapeString('!circus');
        $this->assertStringContainsString('\!', $result);
        $this->assertStringContainsString('circus', $result);
    }

    /** @param array<string, string>|string $input */
    #[Test]
    #[DataProvider('negationDetectionProvider')]
    public function it_detects_negation_operators(array|string $input, bool $expected): void
    {
        $result = ManticoreSearchDriver::queryHasNegation($input);
        $this->assertSame($expected, $result);
    }

    /** @return array<string, array{array<string, string>|string, bool}> */
    public static function negationDetectionProvider(): array
    {
        return [
            'bang negation string' => ['!harry', true],
            'hyphen negation string' => ['-harry', true],
            'word + negation string' => ['dead !circus', true],
            'word + hyphen negation string' => ['dead -circus', true],
            'plain word string' => ['harry', false],
            'multiple words string' => ['harry potter', false],
            'mid-word hyphen string' => ['spider-man', false],
            'empty string' => ['', false],
            'negation in array value' => [['searchname' => '!harry'], true],
            'plain word in array value' => [['searchname' => 'harry'], false],
            'multiple fields with negation' => [['searchname' => 'potter', 'name' => '!harry'], true],
            'empty array' => [[], false],
            'array with -1 values' => [['searchname' => '-1'], false],
        ];
    }

    #[Test]
    public function it_passes_release_search_limit_to_search_indexes(): void
    {
        $driver = new class extends ManticoreSearchDriver
        {
            public int $capturedLimit = 0;

            public function __construct() {}

            public function searchIndexes(string $rt_index, ?string $searchString, array $column = [], array $searchArray = [], int $limit = 1000): array
            {
                $this->capturedLimit = $limit;

                return ['id' => [101, 202]];
            }
        };

        $result = $driver->searchReleases(['searchname' => 'harry potter'], 75);

        $this->assertSame(75, $driver->capturedLimit);
        $this->assertSame([101, 202], $result);
    }

    #[Test]
    public function it_clamps_search_limit_to_configured_max_matches(): void
    {
        $reflection = new ReflectionClass(ManticoreSearchDriver::class);
        $driver = $reflection->newInstanceWithoutConstructor();

        $configProperty = $reflection->getProperty('config');
        $configProperty->setValue($driver, ['max_matches' => 500]);

        $method = $reflection->getMethod('normalizeSearchLimit');

        $this->assertSame(500, $method->invoke($driver, 750));
        $this->assertSame(1, $method->invoke($driver, 0));
    }

    #[Test]
    public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_manticore(): void
    {
        $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');

        $this->assertIsString($driverSource);
        $this->assertStringContainsString("\$query->sort('id', \$order);", $driverSource);
    }

    #[Test]
    public function release_search_query_normalizes_media_info_filters(): void
    {
        $criteria = ReleaseSearchQuery::fromCriteria([
            'has_media_info' => true,
            'media_unique_id' => ' 0x123ABC ',
            'min_video_width' => -1,
            'max_video_width' => 3840,
            'min_video_height' => 720,
            'max_video_height' => 2160,
        ], 25)->criteria();

        $this->assertTrue($criteria['has_media_info']);
        $this->assertSame('0x123ABC', $criteria['media_unique_id']);
        $this->assertSame(0, $criteria['min_video_width']);
        $this->assertSame(3840, $criteria['max_video_width']);
        $this->assertSame(720, $criteria['min_video_height']);
        $this->assertSame(2160, $criteria['max_video_height']);

        $invalidCriteria = ReleaseSearchQuery::fromCriteria(['media_unique_id' => '-1'], 25)->criteria();
        $this->assertNull($invalidCriteria['media_unique_id']);
    }

    #[Test]
    public function filtered_release_search_allowlists_media_info_fields_and_filters(): void
    {
        $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');

        $this->assertIsString($driverSource);
        $this->assertStringContainsString("'media_video_codec',", $driverSource);
        $this->assertStringContainsString('in_array($key, self::RELEASE_FIELD_SEARCH_FIELDS, true)', $driverSource);
        $this->assertStringContainsString("\$query->filter('has_media_info', '='", $driverSource);
        $this->assertStringContainsString("\$query->filter('media_unique_id', '='", $driverSource);
        $this->assertStringContainsString("\$query->filter('media_video_width', 'gte'", $driverSource);
        $this->assertStringContainsString("\$query->filter('media_video_height', 'lte'", $driverSource);

        $reflection = new ReflectionClass(ManticoreSearchDriver::class);
        $this->assertNotContains('media_unique_id', $reflection->getConstant('RELEASE_FIELD_SEARCH_FIELDS'));
    }

    #[Test]
    public function search_releases_filtered_scopes_field_queries_with_grouping_parentheses(): void
    {
        $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');

        $this->assertIsString($driverSource);
        $this->assertStringContainsString(
            "\$terms[] = '@@relaxed '.self::scopeReleaseSearchQuery((string) \$value, \$prepared, '@'.\$key);",
            $driverSource
        );
        $this->assertStringNotContainsString(
            "\$terms[] = '@@relaxed @'.\$key.' '.\$prepared;",
            $driverSource
        );
    }

    #[Test]
    public function search_releases_with_category_filter_scopes_searchname_with_grouping_parentheses(): void
    {
        $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ManticoreSearchDriver.php');

        $this->assertIsString($driverSource);
        $this->assertStringContainsString(
            "\$searchExpr = '@@relaxed '.self::scopeReleaseSearchQuery(\$searchTerm, \$preparedSearch, '@searchname');",
            $driverSource
        );
        $this->assertStringNotContainsString(
            "\$searchExpr = '@@relaxed @searchname '.\$preparedSearch;",
            $driverSource
        );
    }

    #[Test]
    public function search_releases_filtered_uses_stable_id_tiebreak_sort_for_elasticsearch(): void
    {
        $driverSource = file_get_contents(__DIR__.'/../../app/Services/Search/Drivers/ElasticSearchDriver.php');

        $this->assertIsString($driverSource);
        $this->assertStringContainsString("['id' => ['order' => \$order]],", $driverSource);
    }
}
