using Baya.Application.Models.Search;
using Baya.Domain.Entities.Verification;

namespace Baya.Test.Foundation.Search;

/// <summary>
/// End-to-end coverage of the search-index maintainer + SqlNurseSearch over a real EF/SQLite model: the
/// is_searchable predicate, NULL-district geography, gender/price filters, rating sort, the verification
/// flip, service-area fan-out/remove, variant deactivate, and incremental↔rebuild convergence.
/// </summary>
public sealed class SearchIndexTests
{
    private const string Female = "female";
    private const string Male = "male";

    private static NurseSearchCriteria Criteria(
        long categoryId, long cityId, long? districtId = null, string? gender = null,
        long? minPrice = null, long? maxPrice = null, string? priceUnit = null, int page = 1, int pageSize = 50)
        => new(categoryId, cityId, districtId, gender, minPrice, maxPrice, priceUnit, page, pageSize);

    private static void Project(SearchIndexTestHost host, SearchIndexTestHost.SeededNurse nurse)
    {
        host.Maintainer.ReindexNurseAsync(nurse.Profile, nurse.Verification.Status, default).GetAwaiter().GetResult();
        host.Db.SaveChanges();
    }

    [Fact]
    public void IsSearchable_TrueOnlyWhenVerifiedAcceptingNotSuspendedAndVariantActive()
    {
        using var host = new SearchIndexTestHost();

        var good = host.SeedNurse(Female, verified: true, accepting: true, VerificationStatus.Approved, 1000, host.District3Id);
        var unverified = host.SeedNurse(Female, verified: false, accepting: true, VerificationStatus.Pending, 1000, host.District3Id);
        var notAccepting = host.SeedNurse(Female, verified: true, accepting: false, VerificationStatus.Approved, 1000, host.District3Id);
        var suspended = host.SeedNurse(Female, verified: true, accepting: true, VerificationStatus.Suspended, 1000, host.District3Id);
        var inactiveVariant = host.SeedNurse(Female, verified: true, accepting: true, VerificationStatus.Approved, 1000, host.District3Id, variantActive: false);

        foreach (var n in new[] { good, unverified, notAccepting, suspended, inactiveVariant })
            Project(host, n);

        Assert.True(IsSearchable(host, good.NurseId));
        Assert.False(IsSearchable(host, unverified.NurseId));
        Assert.False(IsSearchable(host, notAccepting.NurseId));
        Assert.False(IsSearchable(host, suspended.NurseId));
        Assert.False(IsSearchable(host, inactiveVariant.NurseId));

        // Every nurse with a variant + area has an index row, but only the fully-bookable one is searchable.
        Assert.Equal(5, host.LiveRowCount());
        Assert.Equal(1, host.SearchableRowCount());
    }

    [Fact]
    public void Geography_WholeCityAndDistrictMatchingIsExact()
    {
        using var host = new SearchIndexTestHost();

        var district3 = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        var wholeCity = host.SeedNurse(Male, true, true, VerificationStatus.Approved, 1000, districtId: null);
        Project(host, district3);
        Project(host, wholeCity);

        // District-3 search: the district-3 nurse AND the whole-city (NULL) nurse.
        var d3 = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, host.District3Id), default).Result;
        Assert.Equal(new[] { district3.NurseId, wholeCity.NurseId }.OrderBy(x => x), d3.Items.Select(i => i.NurseId).OrderBy(x => x));

        // A different district in the same city: only the whole-city nurse.
        var d7 = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, host.District7Id), default).Result;
        Assert.Equal(new[] { wholeCity.NurseId }, d7.Items.Select(i => i.NurseId).ToArray());

        // City-only search (no district): both.
        var city = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result;
        Assert.Equal(2, city.Total);
    }

    [Fact]
    public void SameGenderFilterNarrowsResults()
    {
        using var host = new SearchIndexTestHost();
        var female = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        var male = host.SeedNurse(Male, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        Project(host, female);
        Project(host, male);

        var females = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, gender: Female), default).Result;
        Assert.Equal(new[] { female.NurseId }, females.Items.Select(i => i.NurseId).ToArray());

        var males = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, gender: Male), default).Result;
        Assert.Equal(new[] { male.NurseId }, males.Items.Select(i => i.NurseId).ToArray());
    }

    [Fact]
    public void PriceRangeFiltersOnCopiedIrrPrice()
    {
        using var host = new SearchIndexTestHost();
        var cheap = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 500_000, host.District3Id);
        var pricey = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 5_000_000, host.District3Id);
        Project(host, cheap);
        Project(host, pricey);

        var midBand = host.Search.SearchAsync(
            Criteria(host.CategoryId, host.CityId, minPrice: 400_000, maxPrice: 1_000_000), default).Result;

        Assert.Equal(new[] { cheap.NurseId }, midBand.Items.Select(i => i.NurseId).ToArray());
        Assert.Equal("500000", midBand.Items[0].Price);
    }

    [Fact]
    public void ResultsSortByRatingDescending()
    {
        using var host = new SearchIndexTestHost();
        var low = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id, averageRating: 3.1m, totalReviews: 4);
        var high = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id, averageRating: 4.8m, totalReviews: 9);
        Project(host, low);
        Project(host, high);

        var page = host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result;

        Assert.Equal(new[] { high.NurseId, low.NurseId }, page.Items.Select(i => i.NurseId).ToArray());
    }

    [Fact]
    public void SuspendingANurseRemovesThemFromSearch()
    {
        using var host = new SearchIndexTestHost();
        var nurse = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        Project(host, nurse);
        Assert.Single(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result.Items);

        // Flip to suspended + unverified (the b6 suspend path) and reindex in place.
        nurse.Profile.MarkUnverified();
        nurse.Verification.Status = VerificationStatus.Suspended;
        Project(host, nurse);

        Assert.Empty(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result.Items);
        Assert.False(IsSearchable(host, nurse.NurseId));

        // Reinstating makes them searchable again — the row is resurrected, not duplicated.
        nurse.Profile.MarkVerified();
        nurse.Verification.Status = VerificationStatus.Approved;
        Project(host, nurse);
        Assert.Single(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result.Items);
        Assert.Equal(1, host.LiveRowCount());
    }

    [Fact]
    public void FanOutAddsAreaRows_RemoveDropsExactlyThoseRows()
    {
        using var host = new SearchIndexTestHost();
        var nurse = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        Project(host, nurse);
        Assert.Equal(1, host.LiveRowCount());

        // Add a second area (district 7) and fan out.
        host.Maintainer.FanOutServiceAreaAsync(nurse.NurseId, host.CityId, host.District7Id, default).GetAwaiter().GetResult();
        host.Db.SaveChanges();
        Assert.Equal(2, host.SearchableRowCount());
        Assert.Single(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, host.District7Id), default).Result.Items);

        // Remove the district-7 area: only its rows drop; district 3 stays.
        host.Maintainer.RemoveServiceAreaRowsAsync(nurse.NurseId, host.CityId, host.District7Id, default).GetAwaiter().GetResult();
        host.Db.SaveChanges();
        Assert.Empty(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, host.District7Id), default).Result.Items);
        Assert.Single(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId, host.District3Id), default).Result.Items);
    }

    [Fact]
    public void DeactivatingAVariantKeepsRowsButHidesThem()
    {
        using var host = new SearchIndexTestHost();
        var nurse = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id);
        Project(host, nurse);
        Assert.Equal(1, host.SearchableRowCount());

        nurse.Variant.IsActive = false;
        host.Maintainer.ReindexVariantAsync(nurse.Variant, default).GetAwaiter().GetResult();
        host.Db.SaveChanges();

        Assert.Equal(1, host.LiveRowCount());       // row kept
        Assert.Equal(0, host.SearchableRowCount());  // but not searchable
        Assert.Empty(host.Search.SearchAsync(Criteria(host.CategoryId, host.CityId), default).Result.Items);
    }

    [Fact]
    public void IncrementalMaintenanceConvergesWithFullRebuild()
    {
        using var host = new SearchIndexTestHost();

        var a = host.SeedNurse(Female, true, true, VerificationStatus.Approved, 1000, host.District3Id, averageRating: 4.5m);
        var b = host.SeedNurse(Male, true, true, VerificationStatus.Approved, 2000, districtId: null);
        var c = host.SeedNurse(Female, false, true, VerificationStatus.Pending, 3000, host.District7Id);
        Project(host, a);
        Project(host, b);
        Project(host, c);
        // Add a second area to B incrementally — the real service-area row plus the fan-out, as b4 does.
        host.AddArea(b.NurseId, host.District3Id);
        host.Maintainer.FanOutServiceAreaAsync(b.NurseId, host.CityId, host.District3Id, default).GetAwaiter().GetResult();
        host.Db.SaveChanges();

        var incrementalLive = host.LiveRowCount();
        var incrementalSearchable = host.SearchableRowCount();

        // A full rebuild from source must reproduce the same live/searchable row set (convergence).
        var result = host.Maintainer.RebuildAsync(default).GetAwaiter().GetResult();

        Assert.Equal(incrementalLive, host.LiveRowCount());
        Assert.Equal(incrementalSearchable, host.SearchableRowCount());
        Assert.Equal(3, result.NursesProcessed);
        Assert.Equal(incrementalLive, result.RowsWritten);

        // No duplicate (variant × area) rows after rebuild.
        var duplicates = host.Db.Set<Domain.Entities.Search.NurseSearchIndex>()
            .GroupBy(r => new { r.VariantId, r.CityId, r.DistrictId })
            .Any(g => g.Count() > 1);
        Assert.False(duplicates);
    }

    private static bool IsSearchable(SearchIndexTestHost host, long nurseId)
        => host.Db.Set<Domain.Entities.Search.NurseSearchIndex>().Any(r => r.NurseId == nurseId && r.IsSearchable);
}
