<?php

namespace Tests\Feature;

use App\Facades\Settings;
use App\Filter\FilterIndex;
use App\MeilisearchFilter;
use App\Services\MeilisearchService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Schema;
use Mockery\MockInterface;
use Mtc\Filter\Contracts\IsFilter;
use Mtc\MercuryDataModels\VehicleMake;
use Tests\TenantTestCase;

class MeilisearchFilterFacetTest extends TenantTestCase
{
    protected function setUp(): void
    {
        parent::setUp();

        // Enable sort by popularity for these tests (matches original behavior)
        Settings::make([
            'config_key' => 'filter-sort-options-by-popularity',
            'type' => 'boolean',
            'value' => true,
        ]);

        if (!Schema::hasTable('filter_index')) {
            Schema::create('filter_index', function (Blueprint $table) {
                $table->bigIncrements('id');
                $table->string('slug')->unique();
                $table->string('name')->nullable();
                $table->string('filter_type');
                $table->string('filter_id');
                $table->unsignedBigInteger('order')->default(0);
                $table->timestamps();
                $table->index(['filter_type', 'filter_id']);
            });
        }
    }

    /**
     * Test that facet counts from Meilisearch are merged and sorted by count descending.
     */
    public function testFacetCountsAreMergedAndSortedByCountDescending(): void
    {
        // Create mock filter results (VehicleMake models) - intentionally in wrong order
        $ford = new VehicleMake(['name' => 'Ford']);
        $ford->id = 1;
        $ford->slug = 'ford';

        $bmw = new VehicleMake(['name' => 'BMW']);
        $bmw->id = 2;
        $bmw->slug = 'bmw';

        $audi = new VehicleMake(['name' => 'Audi']);
        $audi->id = 3;
        $audi->slug = 'audi';

        // Pass in alphabetical order: Audi, BMW, Ford
        $mockResults = collect([$audi, $bmw, $ford]);

        // Mock the Meilisearch service to return facet distribution
        // Ford has most results, then Audi, then BMW
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 150,
                        'bmw' => 50,
                        'audi' => 98,
                    ],
                ]);
        });

        // Create filter instance with mock
        $filter = $this->app->make(MeilisearchFilter::class);

        // Use reflection to test the protected mergeFacetCounts method
        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('mergeFacetCounts');
        $method->setAccessible(true);

        // Create a mock IsFilter that returns 'slug' as the ID attribute
        $mockFilter = $this->createMock(IsFilter::class);
        $mockFilter->method('getIdAttribute')->willReturn('slug');

        // Call the method
        $result = $method->invoke($filter, $mockResults, 'make', $mockFilter);

        // Assert results are sorted by count descending: Ford (150), Audi (98), BMW (50)
        $this->assertEquals('ford', $result->values()[0]->slug);
        $this->assertEquals(150, $result->values()[0]->result_count);

        $this->assertEquals('audi', $result->values()[1]->slug);
        $this->assertEquals(98, $result->values()[1]->result_count);

        $this->assertEquals('bmw', $result->values()[2]->slug);
        $this->assertEquals(50, $result->values()[2]->result_count);
    }

    /**
     * Test that filter results without matching facet counts get zero count.
     */
    public function testFilterResultsWithoutFacetCountsGetZeroCount(): void
    {
        // Create mock filter results
        $audi = new VehicleMake(['name' => 'Audi']);
        $audi->id = 3;
        $audi->slug = 'audi';

        $mockResults = collect([$audi]);

        // Mock the Meilisearch service to return empty facet distribution for make
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 150,
                        // No 'audi' in facet distribution
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('mergeFacetCounts');
        $method->setAccessible(true);

        $mockFilter = $this->createMock(IsFilter::class);
        $mockFilter->method('getIdAttribute')->willReturn('slug');

        $result = $method->invoke($filter, $mockResults, 'make', $mockFilter);

        // Assert that non-matching filter gets zero count
        $this->assertEquals(0, $result->first()->result_count);
    }

    /**
     * Test that array-based filter results (like age) get counts properly.
     */
    public function testArrayBasedFilterResultsGetCounts(): void
    {
        // Age filter returns arrays, not objects
        $mockResults = collect([
            ['id' => 2024, 'name' => 2024],
            ['id' => 2023, 'name' => 2023],
            ['id' => 2022, 'name' => 2022],
        ]);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'manufacture_year' => [
                        2024 => 50,
                        2023 => 120,
                        2022 => 80,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('mergeFacetCounts');
        $method->setAccessible(true);

        $mockFilter = $this->createMock(IsFilter::class);
        $mockFilter->method('getIdAttribute')->willReturn('id');

        $result = $method->invoke($filter, $mockResults, 'age', $mockFilter);

        // Results should be sorted by count: 2023 (120), 2022 (80), 2024 (50)
        $this->assertEquals(2023, $result->values()[0]['id']);
        $this->assertEquals(120, $result->values()[0]['count']);

        $this->assertEquals(2022, $result->values()[1]['id']);
        $this->assertEquals(80, $result->values()[1]['count']);

        $this->assertEquals(2024, $result->values()[2]['id']);
        $this->assertEquals(50, $result->values()[2]['count']);
    }

    /**
     * Test facet attribute mapping returns correct Meilisearch attribute names.
     */
    public function testFacetAttributeMapping(): void
    {
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $property = $reflection->getProperty('facetAttributeMapping');
        $property->setAccessible(true);

        $mapping = $property->getValue($filter);

        $this->assertEquals('make_slug', $mapping['make']);
        $this->assertEquals('model_slug', $mapping['model']);
        $this->assertEquals('body_style_slug', $mapping['body_type']);
        $this->assertEquals('fuel_type_slug', $mapping['fuel_type']);
        $this->assertEquals('transmission_slug', $mapping['transmission']);
        $this->assertEquals('colour_slug', $mapping['colour']);
        $this->assertEquals('dealership_slug', $mapping['location']);
        $this->assertEquals('manufacture_year', $mapping['age']);
    }

    /**
     * Test that buildMeilisearchFiltersExcluding properly excludes the specified filter.
     */
    public function testBuildFiltersExcludesSpecifiedFilter(): void
    {
        $this->mock(MeilisearchService::class);

        $filter = $this->app->make(MeilisearchFilter::class);

        // Set up selections using reflection
        $reflection = new \ReflectionClass($filter);
        $selectionsProperty = $reflection->getProperty('selections');
        $selectionsProperty->setAccessible(true);
        $selectionsProperty->setValue($filter, [
            'make' => ['ford', 'bmw'],
            'fuel_type' => ['petrol'],
            'body_type' => ['suv'],
        ]);

        // Call the method
        $method = $reflection->getMethod('buildMeilisearchFiltersExcluding');
        $method->setAccessible(true);

        $result = $method->invoke($filter, 'make');

        // Should contain fuel_type and body_type but NOT make
        $this->assertArrayHasKey('fuel_type_slug', $result);
        $this->assertArrayHasKey('body_style_slug', $result);
        $this->assertArrayNotHasKey('make_slug', $result);

        // Base filters should always be present
        $this->assertTrue($result['is_published']);
        $this->assertFalse($result['is_sold']);
    }

    /**
     * Create a mock IsFilter that includes the filterType() method.
     */
    private function createFilterMock(string $idAttr = 'id', string $nameAttr = 'name', ?string $filterType = null, ?string $modelClass = null): IsFilter
    {
        $onlyMethods = ['getIdAttribute', 'getNameAttribute'];
        if ($modelClass !== null) {
            $onlyMethods[] = 'getModel';
        }

        $mock = $this->getMockBuilder(IsFilter::class)
            ->onlyMethods($onlyMethods)
            ->addMethods(['filterType'])
            ->getMockForAbstractClass();

        $mock->method('getIdAttribute')->willReturn($idAttr);
        $mock->method('getNameAttribute')->willReturn($nameAttr);

        if ($filterType !== null) {
            $mock->method('filterType')->willReturn($filterType);
        }

        if ($modelClass !== null) {
            $mock->method('getModel')->willReturn($modelClass);
        }

        return $mock;
    }

    /**
     * Test that buildResultsFromFacets uses filter_index for names and slugs.
     */
    public function testBuildResultsFromFacetsUsesFilterIndex(): void
    {
        // Seed filter_index with make entries
        FilterIndex::create(['slug' => 'ford', 'name' => 'Ford', 'filter_type' => 'make', 'filter_id' => '1']);
        FilterIndex::create(['slug' => 'bmw', 'name' => 'BMW', 'filter_type' => 'make', 'filter_id' => '2']);
        FilterIndex::create(['slug' => 'audi', 'name' => 'Audi', 'filter_type' => 'make', 'filter_id' => '3']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 150,
                        'bmw' => 50,
                        'audi' => 98,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createFilterMock('id', 'name', 'make');

        $result = $method->invoke($filter, 'make', $mockFilter, 'make_slug', 0);

        // Results should have names from filter_index, sorted by count desc
        $this->assertCount(3, $result);

        $this->assertEquals('Ford', $result->values()[0]->name);
        $this->assertEquals(150, $result->values()[0]->result_count);
        $this->assertEquals('ford', $result->values()[0]->slug);

        $this->assertEquals('Audi', $result->values()[1]->name);
        $this->assertEquals(98, $result->values()[1]->result_count);

        $this->assertEquals('BMW', $result->values()[2]->name);
        $this->assertEquals(50, $result->values()[2]->result_count);
    }

    /**
     * Test that buildResultsFromFacets creates minimal entries for missing index entries.
     */
    public function testBuildResultsFromFacetsHandlesMissingIndexEntries(): void
    {
        // Only create one filter_index entry; 'tesla' will be missing
        FilterIndex::create(['slug' => 'ford', 'name' => 'Ford', 'filter_type' => 'make', 'filter_id' => '1']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 100,
                        'tesla' => 50,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createFilterMock('id', 'name', 'make');

        $result = $method->invoke($filter, 'make', $mockFilter, 'make_slug', 0);

        $this->assertCount(2, $result);

        // Ford should have proper name from filter_index
        $fordEntry = $result->values()[0];
        $this->assertEquals('Ford', $fordEntry->name);
        $this->assertEquals(100, $fordEntry->result_count);

        // Tesla should have fallback name = key
        $teslaEntry = $result->values()[1];
        $this->assertEquals('tesla', $teslaEntry->name);
        $this->assertEquals(50, $teslaEntry->result_count);
    }

    /**
     * Test that buildResultsFromFacets backfills names from reference model
     * when filter_index entries are missing (fixes slug-as-name display bug).
     */
    public function testBuildResultsFromFacetsBackfillsFromReferenceModel(): void
    {
        // Create VehicleMake records in the DB but NO filter_index entries
        VehicleMake::query()->insert([
            ['id' => 1, 'name' => 'Ford', 'slug' => 'ford'],
            ['id' => 2, 'name' => 'BMW', 'slug' => 'bmw'],
        ]);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 100,
                        'bmw' => 50,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        // Mock filter with getModel() returning VehicleMake
        $mockFilter = $this->createFilterMock('id', 'name', 'make', VehicleMake::class);

        $result = $method->invoke($filter, 'make', $mockFilter, 'make_slug', 0);

        $this->assertCount(2, $result);
        // Names should come from VehicleMake table, NOT from slugs
        $this->assertEquals('Ford', $result->values()[0]->name);
        $this->assertEquals(100, $result->values()[0]->result_count);
        $this->assertEquals('BMW', $result->values()[1]->name);
        $this->assertEquals(50, $result->values()[1]->result_count);
    }

    /**
     * Test that buildResultsFromFacets returns all results regardless of limit
     * (facet data is already in memory from a single Meilisearch call).
     */
    public function testBuildResultsFromFacetsReturnsAllResults(): void
    {
        FilterIndex::create(['slug' => 'ford', 'name' => 'Ford', 'filter_type' => 'make', 'filter_id' => '1']);
        FilterIndex::create(['slug' => 'bmw', 'name' => 'BMW', 'filter_type' => 'make', 'filter_id' => '2']);
        FilterIndex::create(['slug' => 'audi', 'name' => 'Audi', 'filter_type' => 'make', 'filter_id' => '3']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'make_slug' => [
                        'ford' => 150,
                        'bmw' => 50,
                        'audi' => 98,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createFilterMock('id', 'name', 'make');

        // Even with limit=2, all results should be returned (no truncation)
        $result = $method->invoke($filter, 'make', $mockFilter, 'make_slug', 2);

        $this->assertCount(3, $result);
        $this->assertEquals('Ford', $result->values()[0]->name);
        $this->assertEquals('Audi', $result->values()[1]->name);
        $this->assertEquals('BMW', $result->values()[2]->name);
    }

    /**
     * Test that buildResultsFromFacets returns empty collection when no facets.
     */
    public function testBuildResultsFromFacetsReturnsEmptyWhenNoFacets(): void
    {
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createFilterMock('id', 'name', 'make');

        $result = $method->invoke($filter, 'make', $mockFilter, 'make_slug', 0);

        $this->assertTrue($result->isEmpty());
    }

    /**
     * Test that lookupFilterIndex uses filter_id for ID-based facet attributes.
     */
    public function testLookupFilterIndexUsesFilterIdForIdBasedFacets(): void
    {
        FilterIndex::create(['slug' => 'premium', 'name' => 'Premium', 'filter_type' => 'labels', 'filter_id' => '10']);
        FilterIndex::create(['slug' => 'sale', 'name' => 'On Sale', 'filter_type' => 'labels', 'filter_id' => '20']);

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('lookupFilterIndex');
        $method->setAccessible(true);

        $result = $method->invoke($filter, 'labels', [10, 20], 'label_ids');

        $this->assertCount(2, $result);
        // Keyed by filter_id
        $this->assertEquals('Premium', $result->get('10')->name);
        $this->assertEquals('On Sale', $result->get('20')->name);
    }

    /**
     * Test that lookupFilterIndex uses slug for slug-based facet attributes.
     */
    public function testLookupFilterIndexUsesSlugForSlugBasedFacets(): void
    {
        FilterIndex::create(['slug' => 'ford', 'name' => 'Ford', 'filter_type' => 'make', 'filter_id' => '1']);
        FilterIndex::create(['slug' => 'bmw', 'name' => 'BMW', 'filter_type' => 'make', 'filter_id' => '2']);

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('lookupFilterIndex');
        $method->setAccessible(true);

        $result = $method->invoke($filter, 'make', ['ford', 'bmw'], 'make_slug');

        $this->assertCount(2, $result);
        // Keyed by slug
        $this->assertEquals('Ford', $result->get('ford')->name);
        $this->assertEquals('BMW', $result->get('bmw')->name);
    }

    /**
     * Test that buildResultsFromFacets sets custom id attribute for filters that override getIdAttribute.
     */
    public function testBuildResultsFromFacetsSetsCustomIdAttribute(): void
    {
        FilterIndex::create(['slug' => 'car', 'name' => 'Car', 'filter_type' => 'type', 'filter_id' => '1']);
        FilterIndex::create(['slug' => 'van', 'name' => 'Van', 'filter_type' => 'type', 'filter_id' => '2']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'type' => [
                        'car' => 500,
                        'van' => 100,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        // VehicleTypeFilter has getIdAttribute() = 'type'
        $mockFilter = $this->createFilterMock('type', 'name', 'type');

        $result = $method->invoke($filter, 'vehicle_type', $mockFilter, 'type', 0);

        $this->assertCount(2, $result);
        // The 'type' attribute should be set to the slug value
        $this->assertEquals('car', $result->values()[0]->type);
        $this->assertEquals('van', $result->values()[1]->type);
    }

    /**
     * Test that buildResultsFromFacets sets custom name attribute for filters that override getNameAttribute.
     */
    public function testBuildResultsFromFacetsSetsCustomNameAttribute(): void
    {
        FilterIndex::create(['slug' => 'in-stock', 'name' => 'In Stock', 'filter_type' => 'stock_status', 'filter_id' => '1']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'stock_status' => [
                        'in-stock' => 200,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildResultsFromFacets');
        $method->setAccessible(true);

        // StockStatusFilter has getNameAttribute() = 'value'
        $mockFilter = $this->createFilterMock('id', 'value', 'stock_status');

        $result = $method->invoke($filter, 'stock_status', $mockFilter, 'stock_status', 0);

        $this->assertCount(1, $result);
        // The 'value' attribute should be set to the display name
        $this->assertEquals('In Stock', $result->values()[0]->value);
    }

    /**
     * Test retrieveSingleFilterResults uses facet path for mapped filters.
     */
    public function testRetrieveSingleFilterResultsUsesFacetPathForMappedFilters(): void
    {
        FilterIndex::create(['slug' => 'petrol', 'name' => 'Petrol', 'filter_type' => 'fuel_type', 'filter_id' => '1']);
        FilterIndex::create(['slug' => 'diesel', 'name' => 'Diesel', 'filter_type' => 'fuel_type', 'filter_id' => '2']);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacets')
                ->andReturn([
                    'fuel_type_slug' => [
                        'petrol' => 300,
                        'diesel' => 200,
                    ],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('retrieveSingleFilterResults');
        $method->setAccessible(true);

        $mockFilter = $this->createFilterMock('id', 'name', 'fuel_type');

        $result = $method->invoke($filter, $mockFilter, 'fuel_type');

        $this->assertCount(2, $result);
        $this->assertEquals('Petrol', $result->values()[0]->name);
        $this->assertEquals(300, $result->values()[0]->result_count);
        $this->assertEquals('Diesel', $result->values()[1]->name);
        $this->assertEquals(200, $result->values()[1]->result_count);
    }

    // ── Range filter facet tests ────────────────────────────────────────

    /**
     * Create a mock IsFilter with getSelectionName() for range filter tests.
     */
    private function createRangeFilterMock(callable $selectionNameFn): IsFilter
    {
        $mock = $this->getMockBuilder(IsFilter::class)
            ->onlyMethods(['getIdAttribute', 'getNameAttribute'])
            ->addMethods(['getSelectionName'])
            ->getMockForAbstractClass();

        $mock->method('getIdAttribute')->willReturn('id');
        $mock->method('getNameAttribute')->willReturn('name');
        $mock->method('getSelectionName')->willReturnCallback($selectionNameFn);

        return $mock;
    }

    /**
     * Test that getFacetsWithStats returns both distribution and stats keys.
     */
    public function testGetFacetsWithStatsReturnsBothDistributionAndStats(): void
    {
        $mockSearchResult = $this->createMock(\Meilisearch\Search\SearchResult::class);
        $mockSearchResult->method('getFacetDistribution')->willReturn([
            'price' => [5000 => 10, 10000 => 20],
        ]);
        $mockSearchResult->method('getFacetStats')->willReturn([
            'price' => ['min' => 2500, 'max' => 45000],
        ]);

        $mockIndex = $this->createMock(\Meilisearch\Endpoints\Indexes::class);
        $mockIndex->method('search')->willReturn($mockSearchResult);

        $mockClient = $this->createMock(\Meilisearch\Client::class);
        $mockClient->method('index')->willReturn($mockIndex);

        $service = new MeilisearchService();
        $reflection = new \ReflectionClass($service);
        $clientProp = $reflection->getProperty('client');
        $clientProp->setAccessible(true);
        $clientProp->setValue($service, $mockClient);

        $result = $service->getFacetsWithStats([], ['price']);

        $this->assertArrayHasKey('distribution', $result);
        $this->assertArrayHasKey('stats', $result);
        $this->assertEquals(2500, $result['stats']['price']['min']);
        $this->assertEquals(45000, $result['stats']['price']['max']);
        $this->assertArrayHasKey('price', $result['distribution']);
    }

    /**
     * Test that buildConfigRangeFromFacets filters config ranges by facetStats min/max.
     */
    public function testBuildConfigRangeFromFacetsFiltersByMinMax(): void
    {
        Config::set('automotive.filter-ranges.price', [1000, 2000, 5000, 10000, 20000, 50000]);

        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildConfigRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "£{$v}");

        $facetData = [
            'stats' => ['price' => ['min' => 4500, 'max' => 25000]],
            'distribution' => [],
        ];

        $config = ['config_key' => 'automotive.filter-ranges.price', 'has_value' => true];

        $result = $method->invoke($filter, $mockFilter, 'price', $config, $facetData);

        // With min=4500, max=25000 and ranges [1000, 2000, 5000, 10000, 20000, 50000]:
        // Max filter: include ranges < 25000 or one step over → 1000,2000,5000,10000,20000,50000
        // Min filter: include ranges > 4500 or one step under → 2000,5000,10000,20000,50000
        // Combined: 2000, 5000, 10000, 20000, 50000
        $ids = $result->pluck('id')->toArray();
        $this->assertContains(2000, $ids);
        $this->assertContains(5000, $ids);
        $this->assertContains(10000, $ids);
        $this->assertContains(20000, $ids);
        $this->assertContains(50000, $ids);
        $this->assertNotContains(1000, $ids);
    }

    /**
     * Test that slice_last config removes the last element (for min filters).
     */
    public function testBuildConfigRangeFromFacetsSlicesLastForMinFilters(): void
    {
        Config::set('automotive.filter-ranges.price', [1000, 5000, 10000, 20000, 50000]);

        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildConfigRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "£{$v}");

        $facetData = [
            'stats' => ['price' => ['min' => 500, 'max' => 60000]],
            'distribution' => [],
        ];

        // Without slice_last
        $configNoSlice = ['config_key' => 'automotive.filter-ranges.price'];
        $resultNoSlice = $method->invoke($filter, $mockFilter, 'price', $configNoSlice, $facetData);
        $lastNoSlice = $resultNoSlice->last()['id'];

        // With slice_last
        $configWithSlice = ['config_key' => 'automotive.filter-ranges.price', 'slice_last' => true];
        $resultWithSlice = $method->invoke($filter, $mockFilter, 'price', $configWithSlice, $facetData);

        $this->assertGreaterThan($resultWithSlice->count(), $resultNoSlice->count());
        $this->assertNotEquals($lastNoSlice, $resultWithSlice->last()['id']);
    }

    /**
     * Test that has_value config flag adds value field to results.
     */
    public function testBuildConfigRangeFromFacetsIncludesValueField(): void
    {
        Config::set('automotive.filter-ranges.price', [1000, 5000, 10000]);

        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildConfigRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "£{$v}");

        $facetData = [
            'stats' => ['price' => ['min' => 500, 'max' => 15000]],
            'distribution' => [],
        ];

        // With has_value
        $configWithValue = ['config_key' => 'automotive.filter-ranges.price', 'has_value' => true];
        $result = $method->invoke($filter, $mockFilter, 'price', $configWithValue, $facetData);
        $this->assertArrayHasKey('value', $result->first());
        $this->assertEquals($result->first()['id'], $result->first()['value']);

        // Without has_value
        $configNoValue = ['config_key' => 'automotive.filter-ranges.price'];
        $result = $method->invoke($filter, $mockFilter, 'price', $configNoValue, $facetData);
        $this->assertArrayNotHasKey('value', $result->first());
    }

    /**
     * Test that buildDiscreteRangeFromFacets returns distinct values from distribution.
     */
    public function testBuildDiscreteRangeFromFacetsReturnsDistinctValues(): void
    {
        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildDiscreteRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "Year From {$v}");

        $facetData = [
            'stats' => [],
            'distribution' => [
                'manufacture_year' => [
                    '2024' => 50,
                    '2023' => 120,
                    '2022' => 80,
                    '2021' => 30,
                ],
            ],
        ];

        $config = ['sort' => 'desc'];
        $result = $method->invoke($filter, $mockFilter, 'manufacture_year', $config, $facetData);

        $this->assertCount(4, $result);
        $this->assertEquals(2024, $result->first()['id']);
        $this->assertEquals('Year From 2024', $result->first()['name']);
    }

    /**
     * Test that discrete filters with min_value exclude values below the threshold.
     */
    public function testBuildDiscreteRangeFromFacetsFiltersByMinValue(): void
    {
        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildDiscreteRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "From {$v} Doors");

        $facetData = [
            'stats' => [],
            'distribution' => [
                'door_count' => [
                    '0' => 5,
                    '2' => 30,
                    '3' => 100,
                    '4' => 200,
                    '5' => 80,
                ],
            ],
        ];

        $config = ['sort' => 'asc', 'min_value' => 1];
        $result = $method->invoke($filter, $mockFilter, 'door_count', $config, $facetData);

        // Should exclude 0 (below min_value of 1)
        $ids = $result->pluck('id')->toArray();
        $this->assertNotContains(0, $ids);
        $this->assertContains(2, $ids);
        $this->assertContains(3, $ids);
        $this->assertContains(4, $ids);
        $this->assertContains(5, $ids);
        $this->assertCount(4, $result);
    }

    /**
     * Test that discrete filters sort correctly: desc for years, asc for doors/seats.
     */
    public function testBuildDiscreteRangeFromFacetsSortsCorrectly(): void
    {
        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildDiscreteRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => (string) $v);

        $facetData = [
            'stats' => [],
            'distribution' => [
                'manufacture_year' => ['2022' => 10, '2024' => 20, '2023' => 15],
                'door_count' => ['5' => 10, '2' => 20, '3' => 15],
            ],
        ];

        // Years sorted descending
        $yearConfig = ['sort' => 'desc'];
        $yearResult = $method->invoke($filter, $mockFilter, 'manufacture_year', $yearConfig, $facetData);
        $yearIds = $yearResult->pluck('id')->toArray();
        $this->assertEquals([2024, 2023, 2022], $yearIds);

        // Doors sorted ascending
        $doorConfig = ['sort' => 'asc'];
        $doorResult = $method->invoke($filter, $mockFilter, 'door_count', $doorConfig, $facetData);
        $doorIds = $doorResult->pluck('id')->toArray();
        $this->assertEquals([2, 3, 5], $doorIds);
    }

    /**
     * Test that retrieveSingleFilterResults routes range filters through the facet path.
     */
    public function testRetrieveSingleFilterResultsUsesRangeFacetPath(): void
    {
        Config::set('automotive.filter-ranges.price', [1000, 5000, 10000, 20000, 50000]);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacetsWithStats')
                ->once()
                ->andReturn([
                    'distribution' => [],
                    'stats' => ['price' => ['min' => 3000, 'max' => 30000]],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('retrieveSingleFilterResults');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "Price From £{$v}");

        $result = $method->invoke($filter, $mockFilter, 'price_min');

        // Should return config ranges filtered by stats, with slice_last applied
        $this->assertInstanceOf(Collection::class, $result);
        $this->assertNotEmpty($result);
        // Each entry should have id and name keys
        $this->assertArrayHasKey('id', $result->first());
        $this->assertArrayHasKey('name', $result->first());
    }

    /**
     * Test that buildConfigRangeFromFacets returns empty when no stats available.
     */
    public function testBuildConfigRangeFromFacetsReturnsEmptyWhenNoStats(): void
    {
        $this->mock(MeilisearchService::class);
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('buildConfigRangeFromFacets');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "£{$v}");

        $facetData = [
            'stats' => [],
            'distribution' => [],
        ];

        $config = ['config_key' => 'automotive.filter-ranges.price'];
        $result = $method->invoke($filter, $mockFilter, 'price', $config, $facetData);

        $this->assertTrue($result->isEmpty());
    }

    /**
     * Test that getRangeFacetData excludes range filter constraints
     * but includes categorical filter constraints.
     */
    public function testGetRangeFacetDataExcludesRangeConstraints(): void
    {
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacetsWithStats')
                ->once()
                ->withArgs(function (array $filters) {
                    // Should include categorical filters
                    $hasMake = isset($filters['make_slug']);
                    // Should NOT include range filter values
                    $hasPriceMin = isset($filters['price']);
                    $hasManufactureYear = isset($filters['manufacture_year']);

                    return $hasMake && !$hasPriceMin && !$hasManufactureYear;
                })
                ->andReturn([
                    'distribution' => [],
                    'stats' => [],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $selectionsProperty = $reflection->getProperty('selections');
        $selectionsProperty->setAccessible(true);
        $selectionsProperty->setValue($filter, [
            'make' => ['ford'],
            'price_min' => ['5000'],
            'manufacture_year_min' => ['2020'],
        ]);

        $method = $reflection->getMethod('getRangeFacetData');
        $method->setAccessible(true);

        $method->invoke($filter);
    }

    /**
     * Test that the consumption (mpg) filter is routed through the range facet path.
     */
    public function testConsumptionFilterUsesRangeFacetPath(): void
    {
        Config::set('automotive.filter-ranges.mpg', [10, 15, 20, 25, 30, 35, 40, 50, 60]);

        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacetsWithStats')
                ->once()
                ->andReturn([
                    'distribution' => [],
                    'stats' => ['mpg' => ['min' => 12, 'max' => 55]],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('retrieveSingleFilterResults');
        $method->setAccessible(true);

        $mockFilter = $this->createRangeFilterMock(fn($v) => "{$v}+ mpg");

        $result = $method->invoke($filter, $mockFilter, 'consumption');

        $this->assertInstanceOf(Collection::class, $result);
        $this->assertNotEmpty($result);
        $this->assertArrayHasKey('id', $result->first());
        $this->assertArrayHasKey('value', $result->first());
        $this->assertArrayHasKey('name', $result->first());
    }

    /**
     * Test that consumption filter is included in rangeFilterFacetConfig.
     */
    public function testRangeFilterFacetConfigIncludesConsumption(): void
    {
        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $property = $reflection->getProperty('rangeFilterFacetConfig');
        $property->setAccessible(true);

        $config = $property->getValue($filter);

        $this->assertArrayHasKey('consumption', $config);
        $this->assertEquals('mpg', $config['consumption']['attr']);
        $this->assertEquals('config', $config['consumption']['type']);
        $this->assertEquals('automotive.filter-ranges.mpg', $config['consumption']['config_key']);
    }

    /**
     * Test that getCheapestOffer uses Meilisearch facet stats instead of a DB query.
     */
    public function testGetCheapestOfferUsesFacetStats(): void
    {
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacetsWithStats')
                ->once()
                ->andReturn([
                    'distribution' => [],
                    'stats' => ['price' => ['min' => 2999.99, 'max' => 85000]],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('getCheapestOffer');
        $method->setAccessible(true);

        $result = $method->invoke($filter);

        $this->assertEquals(2999.99, $result);
    }

    /**
     * Test that getCheapestOffer returns null when no price stats available.
     */
    public function testGetCheapestOfferReturnsNullWhenNoStats(): void
    {
        $this->mock(MeilisearchService::class, function (MockInterface $mock) {
            $mock->shouldReceive('getFacetsWithStats')
                ->once()
                ->andReturn([
                    'distribution' => [],
                    'stats' => [],
                ]);
        });

        $filter = $this->app->make(MeilisearchFilter::class);

        $reflection = new \ReflectionClass($filter);
        $method = $reflection->getMethod('getCheapestOffer');
        $method->setAccessible(true);

        $result = $method->invoke($filter);

        $this->assertNull($result);
    }
}
