using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftModels.DTOs;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using Models.Common;

namespace DeepDrftTests;

/// <summary>
/// Query-shape tests for the release-cardinal browse path that backs the public /archive browser
/// (Phase 9 §8.H). Exercises <see cref="ReleaseRepository.GetPagedByMediumAsync"/>: the medium
/// narrowing, the genre filter, and the null/empty-filter passthrough.
///
/// Provider note: these run on the EF in-memory provider, which executes LINQ in process. That covers
/// the medium predicate, exact genre equality, and the count/ordering — every predicate except the
/// free-text branch. That branch uses <c>EF.Functions.ILike</c>, an Npgsql-only relational function
/// with no in-memory translation, so the SearchText case is a Postgres integration test gated on a DSN
/// (<see cref="GetPagedByMediumAsync_WithSearchText_MatchesTitleOrArtistCaseInsensitive"/>). Without a
/// configured database it is ignored rather than asserted against a provider that never runs the
/// predicate — mirroring <see cref="TrackFilterQueryTests"/>.
/// </summary>
[TestFixture]
public class ReleaseBrowseQueryTests
{
    private DeepDrftContext _context = null!;

    [SetUp]
    public void SetUp()
    {
        var options = new DbContextOptionsBuilder<DeepDrftContext>()
            .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
            .Options;
        _context = new DeepDrftContext(options);
    }

    [TearDown]
    public void TearDown() => _context.Dispose();

    private ReleaseRepository CreateRepository()
        => new(_context, NullLogger<ReleaseRepository>.Instance);

    private static ReleaseEntity Release(
        string title, string artist, ReleaseMedium medium = ReleaseMedium.Cut, string? genre = null)
        => new()
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            Title = title,
            Artist = artist,
            Medium = medium,
            Genre = genre,
        };

    private async Task SeedAsync(params ReleaseEntity[] releases)
    {
        _context.Releases.AddRange(releases);
        await _context.SaveChangesAsync();
    }

    private static PagingParameters<ReleaseEntity> DefaultPaging()
        => new() { Page = 1, PageSize = 20, OrderBy = r => r.Id, IsDescending = false };

    // Medium filter narrows to a single medium: only releases of that medium are returned, and
    // TotalCount reflects the filtered set, not the table.
    [Test]
    public async Task GetPagedByMediumAsync_WithMedium_ReturnsOnlyThatMedium()
    {
        await SeedAsync(
            Release("Cut One", "A", ReleaseMedium.Cut),
            Release("Session One", "B", ReleaseMedium.Session),
            Release("Mix One", "C", ReleaseMedium.Mix),
            Release("Session Two", "D", ReleaseMedium.Session));

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), ReleaseMedium.Session, filter: null, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(2));
        Assert.That(result.Items.Select(r => r.Title), Is.EquivalentTo(new[] { "Session One", "Session Two" }));
    }

    // Clearing the medium filter (null) shows every medium — the all-releases archive default.
    [Test]
    public async Task GetPagedByMediumAsync_WithNullMedium_ReturnsAllMedia()
    {
        await SeedAsync(
            Release("Cut One", "A", ReleaseMedium.Cut),
            Release("Session One", "B", ReleaseMedium.Session),
            Release("Mix One", "C", ReleaseMedium.Mix));

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), medium: null, filter: null, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(3));
        Assert.That(result.Items.Select(r => r.Title),
            Is.EquivalentTo(new[] { "Cut One", "Session One", "Mix One" }));
    }

    // Genre filter narrows across all media: only releases of that exact genre are returned, regardless
    // of medium, and TotalCount reflects the filtered set.
    [Test]
    public async Task GetPagedByMediumAsync_WithGenre_ReturnsOnlyThatGenreAcrossMedia()
    {
        await SeedAsync(
            Release("Cut One", "A", ReleaseMedium.Cut, genre: "Techno"),
            Release("Session One", "B", ReleaseMedium.Session, genre: "House"),
            Release("Mix One", "C", ReleaseMedium.Mix, genre: "Techno"));

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), medium: null, new ReleaseFilter { Genre = "Techno" }, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(2));
        Assert.That(result.Items.Select(r => r.Title), Is.EquivalentTo(new[] { "Cut One", "Mix One" }));
    }

    // Medium and genre compose: the result is the intersection, narrowed both by medium and genre.
    [Test]
    public async Task GetPagedByMediumAsync_WithMediumAndGenre_ComposesBothPredicates()
    {
        await SeedAsync(
            Release("Mix Techno", "A", ReleaseMedium.Mix, genre: "Techno"),
            Release("Mix House", "B", ReleaseMedium.Mix, genre: "House"),
            Release("Cut Techno", "C", ReleaseMedium.Cut, genre: "Techno"));

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), ReleaseMedium.Mix, new ReleaseFilter { Genre = "Techno" }, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(1));
        Assert.That(result.Items.Single().Title, Is.EqualTo("Mix Techno"));
    }

    // A null filter is a passthrough: same items and count as no filter at all. An all-null
    // ReleaseFilter is collapsed to null by the manager, so the repository sees null here.
    [Test]
    public async Task GetPagedByMediumAsync_WithNullFilter_ReturnsAllReleases()
    {
        await SeedAsync(
            Release("Cut One", "A", ReleaseMedium.Cut),
            Release("Session One", "B", ReleaseMedium.Session),
            Release("Mix One", "C", ReleaseMedium.Mix));

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), medium: null, filter: null, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(3));
    }

    // Soft-deleted releases never surface in the browse list, with or without a filter.
    [Test]
    public async Task GetPagedByMediumAsync_ExcludesSoftDeletedReleases()
    {
        var deleted = Release("Gone", "A", ReleaseMedium.Cut, genre: "Techno");
        deleted.IsDeleted = true;
        await SeedAsync(
            Release("Live", "B", ReleaseMedium.Cut, genre: "Techno"),
            deleted);

        var repo = CreateRepository();
        var result = await repo.GetPagedByMediumAsync(
            DefaultPaging(), medium: null, new ReleaseFilter { Genre = "Techno" }, CancellationToken.None);

        Assert.That(result.TotalCount, Is.EqualTo(1));
        Assert.That(result.Items.Single().Title, Is.EqualTo("Live"));
    }

    // Free-text search across Title and Artist, case-insensitive. EF.Functions.ILike is Npgsql-only and
    // does not translate on the in-memory provider, so this runs only against a real Postgres database
    // supplied via the DEEPDRFT_TEST_PG environment variable. Without it the test is ignored rather than
    // asserted against a provider that cannot execute the predicate.
    [Test]
    public async Task GetPagedByMediumAsync_WithSearchText_MatchesTitleOrArtistCaseInsensitive()
    {
        var dsn = Environment.GetEnvironmentVariable("DEEPDRFT_TEST_PG");
        if (string.IsNullOrWhiteSpace(dsn))
            Assert.Ignore("Set DEEPDRFT_TEST_PG to a Postgres connection string to run the ILike search test.");

        var options = new DbContextOptionsBuilder<DeepDrftContext>()
            .UseNpgsql(dsn)
            .Options;
        await using var pg = new DeepDrftContext(options);
        await pg.Database.EnsureCreatedAsync();

        try
        {
            pg.Releases.AddRange(
                Release("Nightfall", "jazzmin", ReleaseMedium.Session),
                Release("All JAZZ Hands", "Brick", ReleaseMedium.Cut),
                Release("Silence", "Nobody", ReleaseMedium.Mix));
            await pg.SaveChangesAsync();

            var repo = new ReleaseRepository(pg, NullLogger<ReleaseRepository>.Instance);
            var result = await repo.GetPagedByMediumAsync(
                DefaultPaging(), medium: null, new ReleaseFilter { SearchText = "jazz" }, CancellationToken.None);

            Assert.That(result.Items.Select(r => r.Title),
                Is.EquivalentTo(new[] { "Nightfall", "All JAZZ Hands" }),
                "ILike matches 'jazz' case-insensitively in the release Title or Artist");
        }
        finally
        {
            await pg.Database.EnsureDeletedAsync();
        }
    }
}
