using Data.Data.Repositories;
using DeepDrftData;
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>
/// Backs the public read path that the /cuts/{id} album page consumes (Phase 11 §3a, §3.3).
/// <c>CutDetailViewModel.Load</c> fetches an album's tracks through the releaseId-filtered track page
/// sorted by "TrackNumber"; that maps to <see cref="TrackRepository.GetPagedFilteredAsync"/> with a
/// <see cref="TrackFilter.ReleaseId"/> predicate and an <c>OrderBy(t =&gt; t.TrackNumber)</c>
/// expression. These tests exercise that exact query — the join narrowing, the explicit-ordinal
/// ordering (not insertion order), and the projection of TrackNumber onto the DTO the page renders.
///
/// Provider note: runs on the EF in-memory provider, which executes the ReleaseId equality, the
/// ordinal sort, and the count in process — every predicate this path uses (no ILike branch here).
/// Mirrors <see cref="TrackFilterQueryTests"/>.
/// </summary>
[TestFixture]
public class CutDetailTrackOrderingTests
{
    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)
        => new() { EntryKey = Guid.NewGuid().ToString("N"), Title = title, Artist = artist };

    // A track linked to the given release with an explicit ordinal.
    private static TrackEntity Track(string name, int trackNumber, ReleaseEntity? release = null)
        => new()
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            TrackName = name,
            TrackNumber = trackNumber,
            Release = release,
        };

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

    // The album page's query: filter to one release, order by the explicit ordinal.
    private static PagingParameters<TrackEntity> OrderedByTrackNumber()
        => new() { Page = 1, PageSize = 100, OrderBy = t => t.TrackNumber, IsDescending = false };

    // The release-id filter narrows to that album only — a sibling release's tracks never leak in.
    [Test]
    public async Task ReleaseIdFilter_ReturnsOnlyThatReleasesTracks()
    {
        var albumA = Release("Album A", "Artist");
        var albumB = Release("Album B", "Artist");
        await SeedAsync(
            Track("A-one", 1, albumA),
            Track("A-two", 2, albumA),
            Track("B-one", 1, albumB),
            Track("Loose", 1));

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(
            OrderedByTrackNumber(), new TrackFilter { ReleaseId = albumA.Id });

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

    // The ordering is by the explicit ordinal, not insertion order: tracks seeded out of order
    // come back ascending by TrackNumber. This is the guarantee /cuts/{id} relies on for its rows.
    [Test]
    public async Task OrderByTrackNumber_SortsByExplicitOrdinalNotInsertionOrder()
    {
        var album = Release("Album", "Artist");
        // Insert deliberately scrambled relative to the intended track order.
        await SeedAsync(
            Track("Third", 3, album),
            Track("First", 1, album),
            Track("Second", 2, album));

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(
            OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id });

        Assert.That(
            result.Items.Select(t => t.TrackName).ToList(),
            Is.EqualTo(new[] { "First", "Second", "Third" }),
            "rows must order by the explicit TrackNumber ordinal, not the order they were inserted");
        Assert.That(
            result.Items.Select(t => t.TrackNumber).ToList(),
            Is.EqualTo(new[] { 1, 2, 3 }));
    }

    // The DTO the page renders carries the ordinal — TrackConverter projects TrackNumber onto
    // TrackDto, so the row's number label and the saved order survive the entity -> DTO mapping.
    [Test]
    public async Task TrackConverter_ProjectsTrackNumberOntoDto()
    {
        var album = Release("Album", "Artist");
        await SeedAsync(
            Track("First", 1, album),
            Track("Second", 2, album));

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(
            OrderedByTrackNumber(), new TrackFilter { ReleaseId = album.Id });

        var dtos = result.Items.Select(TrackConverter.Convert).ToList();

        Assert.That(dtos.Select(d => d.TrackNumber).ToList(), Is.EqualTo(new[] { 1, 2 }));
        Assert.That(dtos.Select(d => d.TrackName).ToList(), Is.EqualTo(new[] { "First", "Second" }));
    }

    // An album with no streamable tracks yields an empty page (no rows, no error) — the page header
    // still renders; the track list is simply empty.
    [Test]
    public async Task ReleaseIdFilter_WithNoTracks_ReturnsEmptyPage()
    {
        var empty = Release("Empty Album", "Artist");
        var other = Release("Other", "Artist");
        await SeedAsync(Track("Other-one", 1, other));
        // Persist the empty release with no tracks linked to it.
        _context.Releases.Add(empty);
        await _context.SaveChangesAsync();

        var repo = CreateRepository();
        var result = await repo.GetPagedFilteredAsync(
            OrderedByTrackNumber(), new TrackFilter { ReleaseId = empty.Id });

        Assert.That(result.TotalCount, Is.EqualTo(0));
        Assert.That(result.Items, Is.Empty);
    }
}
