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

namespace DeepDrftTests;

/// <summary>
/// Query-shape tests for the filter and distinct-browse repository methods, updated for the
/// Phase 8 §8.0 normalized schema: release-cardinal data (Artist, Album→Title, Genre, ImagePath)
/// lives on <see cref="ReleaseEntity"/>, reached through the nullable Release navigation. Tracks
/// link via <c>ReleaseId</c>; loose tracks have a null release.
///
/// Provider note: these run on the EF in-memory provider, which executes LINQ in process. That
/// covers exact-match equality through the navigation, null passthrough, GroupBy/Count, and
/// ordering — every predicate in <see cref="TrackRepository.GetPagedFilteredAsync"/> 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 SearchText_*). It is ignored when no test database is configured rather
/// than asserted against a provider that never runs the predicate.
/// </summary>
[TestFixture]
public class TrackFilterQueryTests
{
    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 TrackRepository CreateRepository()
        => new(_context, NullLogger<Repository<DeepDrftContext, TrackEntity>>.Instance);

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

    // A track linked to the given release (or loose when release is null).
    private static TrackEntity Track(string name, ReleaseEntity? release = null)
        => new()
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            TrackName = name,
            Release = release,
        };

    private async Task SeedAsync(params TrackEntity[] tracks)
    {
        _context.Tracks.AddRange(tracks);
        await _context.SaveChangesAsync();
    }

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

    // Case 2 — exact album match: returns only rows whose linked release Title equals the filter
    // value, and TotalCount reflects the filtered set, not the table.
    [Test]
    public async Task GetPagedFilteredAsync_WithExactAlbum_ReturnsOnlyThatAlbum()
    {
        var blue = Release("Blue", "A");
        var red = Release("Red", "C");
        await SeedAsync(
            Track("One", blue),
            Track("Two", blue),
            Track("Three", red),
            Track("Four"));

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { Album = "Blue" });

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

    // Case 2b — exact genre match composes the same way as album, through the release join.
    [Test]
    public async Task GetPagedFilteredAsync_WithExactGenre_ReturnsOnlyThatGenre()
    {
        await SeedAsync(
            Track("One", Release("A1", "A", genre: "Techno")),
            Track("Two", Release("A2", "B", genre: "House")),
            Track("Three", Release("A3", "C", genre: "Techno")));

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { Genre = "Techno" });

        Assert.That(result.TotalCount, Is.EqualTo(2));
        Assert.That(result.Items.Select(t => t.TrackName), Is.EquivalentTo(new[] { "One", "Three" }));
    }

    // Case 3 — null filter is a passthrough: same items and count as the unfiltered base GetPagedAsync.
    [Test]
    public async Task GetPagedFilteredAsync_WithNullFilter_MatchesUnfilteredPagedQuery()
    {
        await SeedAsync(
            Track("One", Release("Blue", "A")),
            Track("Two", Release("Red", "B")),
            Track("Three"));

        var repo = CreateRepository();
        var baseline = await repo.GetPagedAsync(DefaultPaging());
        var filtered = await repo.GetPagedFilteredAsync(DefaultPaging(), filter: null);

        Assert.That(filtered.TotalCount, Is.EqualTo(baseline.TotalCount));
        Assert.That(
            filtered.Items.Select(t => t.Id),
            Is.EqualTo(baseline.Items.Select(t => t.Id)).AsCollection);
    }

    // Case 4 — releases: returns every non-deleted release, title-ascending. Replaces the old
    // distinct-albums grouping; one row per release rather than per distinct album string.
    [Test]
    public async Task GetReleasesAsync_ReturnsAllReleasesTitleAscending()
    {
        await SeedAsync(
            Track("One", Release("Zephyr", "A", image: "cover-z")),
            Track("Two", Release("Aria", "B", image: "cover-a")),
            Track("Three"));

        var repo = CreateRepository();
        var releases = await repo.GetReleasesAsync();

        Assert.That(releases.Select(r => r.Title), Is.EqualTo(new[] { "Aria", "Zephyr" }).AsCollection,
            "releases sort by title ascending; the loose track contributes none");
        Assert.That(releases.Single(r => r.Title == "Zephyr").ImagePath, Is.EqualTo("cover-z"));
        Assert.That(releases.Single(r => r.Title == "Aria").ImagePath, Is.EqualTo("cover-a"));
    }

    // Case 4b — per-release track counts: keyed by ReleaseId, counting only non-deleted tracks.
    // Loose tracks (null ReleaseId) contribute no entry. Backs ReleaseDto.TrackCount.
    [Test]
    public async Task GetTrackCountsByReleaseAsync_CountsTracksPerRelease()
    {
        var zephyr = Release("Zephyr", "A");
        var aria = Release("Aria", "B");
        await SeedAsync(
            Track("One", zephyr),
            Track("Two", zephyr),
            Track("Three", aria),
            Track("Four"));

        var repo = CreateRepository();
        var counts = await repo.GetTrackCountsByReleaseAsync();

        Assert.That(counts[zephyr.Id], Is.EqualTo(2));
        Assert.That(counts[aria.Id], Is.EqualTo(1));
        Assert.That(counts.Count, Is.EqualTo(2), "the loose track contributes no release key");
    }

    // Case 5 — distinct genres: sourced from the release join, excludes releases with null genre,
    // counts tracks per genre, ordered genre ascending.
    [Test]
    public async Task GetDistinctGenresAsync_GroupsCountsAndExcludesNull()
    {
        await SeedAsync(
            Track("One", Release("A1", "A", genre: "Techno")),
            Track("Two", Release("A2", "B", genre: "Ambient")),
            Track("Three", Release("A3", "C", genre: "Techno")),
            Track("Four", Release("A4", "D")));

        var repo = CreateRepository();
        var genres = await repo.GetDistinctGenresAsync();

        Assert.That(genres.Select(g => g.Genre), Is.EqualTo(new[] { "Ambient", "Techno" }).AsCollection,
            "genres sort ascending and the null-genre release is excluded");
        Assert.That(genres.Single(g => g.Genre == "Techno").TrackCount, Is.EqualTo(2));
        Assert.That(genres.Single(g => g.Genre == "Ambient").TrackCount, Is.EqualTo(1));
    }

    // Case 6 — find-or-create resolution: an existing (title, artist) returns the stored row, no
    // duplicate insert. Exercises the natural-key lookup that backs the upload FK resolution.
    [Test]
    public async Task GetReleaseByTitleAndArtistAsync_ReturnsExistingMatch()
    {
        var blue = Release("Blue", "Artist A");
        await SeedAsync(Track("One", blue));

        var repo = CreateRepository();
        var found = await repo.GetReleaseByTitleAndArtistAsync("Blue", "Artist A");

        Assert.That(found, Is.Not.Null);
        Assert.That(found!.Id, Is.EqualTo(blue.Id));
    }

    // Case 6b — no match returns null so the manager creates a fresh release.
    [Test]
    public async Task GetReleaseByTitleAndArtistAsync_ReturnsNullWhenNoMatch()
    {
        await SeedAsync(Track("One", Release("Blue", "Artist A")));

        var repo = CreateRepository();
        var found = await repo.GetReleaseByTitleAndArtistAsync("Red", "Artist A");

        Assert.That(found, Is.Null);
    }

    // Case 1 — free-text search across TrackName plus the joined release Artist/Title,
    // 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 GetPagedFilteredAsync_WithSearchText_MatchesNameArtistOrAlbumCaseInsensitive()
    {
        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.Tracks.AddRange(
                Track("Jazz Odyssey", Release("Smell the Glove", "Spinal Tap")),
                Track("Quiet Storm", Release("Nightfall", "jazzmin")),
                Track("Loud Noises", Release("All JAZZ Hands", "Brick")),
                Track("Unrelated", Release("Silence", "Nobody")));
            await pg.SaveChangesAsync();

            var repo = new TrackRepository(
                pg, NullLogger<Repository<DeepDrftContext, TrackEntity>>.Instance);
            var result = await repo.GetPagedFilteredAsync(DefaultPaging(), new TrackFilter { SearchText = "jazz" });

            Assert.That(result.Items.Select(t => t.TrackName),
                Is.EquivalentTo(new[] { "Jazz Odyssey", "Quiet Storm", "Loud Noises" }),
                "ILike matches 'jazz' case-insensitively in TrackName, release Artist, or release Title");
        }
        finally
        {
            await pg.Database.EnsureDeletedAsync();
        }
    }
}
