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

namespace DeepDrftTests;

/// <summary>
/// Aggregate-query tests for the public home hero stat row, exercising
/// <see cref="TrackRepository.GetHomeStatsAsync"/>: the Cut track count, the per-ReleaseType Cut
/// release breakdown (zero-count types absent), the Mix release count, and the Mix-runtime sum
/// (null durations contributing 0). Runs on the EF in-memory provider like
/// <see cref="ReleaseBrowseQueryTests"/> — every predicate here (Count, GroupBy, Sum with a
/// null-coalesce) translates in process.
/// </summary>
[TestFixture]
public class HomeStatsQueryTests
{
    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, ReleaseMedium medium, ReleaseType releaseType = ReleaseType.Single)
        => new()
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            Title = title,
            Artist = "A",
            Medium = medium,
            ReleaseType = releaseType,
        };

    private static TrackEntity Track(ReleaseEntity release, double? duration = null)
        => new()
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            TrackName = "T",
            Release = release,
            DurationSeconds = duration,
        };

    private async Task SeedAsync(IEnumerable<ReleaseEntity> releases, IEnumerable<TrackEntity> tracks)
    {
        _context.Releases.AddRange(releases);
        _context.Tracks.AddRange(tracks);
        await _context.SaveChangesAsync();
    }

    // Cut track count reflects only tracks whose release is the Cut medium — Session and Mix tracks
    // are excluded — given a mix of all three media.
    [Test]
    public async Task GetHomeStatsAsync_CutTrackCount_CountsOnlyCutMediumTracks()
    {
        var cut = Release("Cut", ReleaseMedium.Cut, ReleaseType.Album);
        var session = Release("Session", ReleaseMedium.Session);
        var mix = Release("Mix", ReleaseMedium.Mix);
        await SeedAsync(
            new[] { cut, session, mix },
            new[] { Track(cut), Track(cut), Track(session), Track(mix) });

        var stats = await CreateRepository().GetHomeStatsAsync();

        Assert.That(stats.CutTrackCount, Is.EqualTo(2));
    }

    // The Cut release-type breakdown groups by ReleaseType, and a type with zero Cut releases is absent
    // from the result entirely (not present-with-zero).
    [Test]
    public async Task GetHomeStatsAsync_CutReleaseTypeBreakdown_OmitsZeroCountTypes()
    {
        await SeedAsync(
            new[]
            {
                Release("Cut Single 1", ReleaseMedium.Cut, ReleaseType.Single),
                Release("Cut Single 2", ReleaseMedium.Cut, ReleaseType.Single),
                Release("Cut Album", ReleaseMedium.Cut, ReleaseType.Album),
                // A Mix release with ReleaseType.EP must not leak into the Cut breakdown.
                Release("Mix EP", ReleaseMedium.Mix, ReleaseType.EP),
            },
            Array.Empty<TrackEntity>());

        var stats = await CreateRepository().GetHomeStatsAsync();

        Assert.That(stats.CutReleaseTypeCounts.Any(c => c.ReleaseType == ReleaseType.EP), Is.False,
            "EP has zero Cut releases and must be absent, not present-with-zero");
        Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Single).Count, Is.EqualTo(2));
        Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1));
    }

    // Mix release count counts Mix-medium releases, and the runtime sum tolerates null durations:
    // not-yet-backfilled tracks contribute 0 rather than throwing or skewing the total.
    [Test]
    public async Task GetHomeStatsAsync_MixRuntime_TreatsNullDurationsAsZero()
    {
        var mixA = Release("Mix A", ReleaseMedium.Mix);
        var mixB = Release("Mix B", ReleaseMedium.Mix);
        var cut = Release("Cut", ReleaseMedium.Cut);
        await SeedAsync(
            new[] { mixA, mixB, cut },
            new[]
            {
                Track(mixA, duration: 600d),
                Track(mixB, duration: null),   // not yet backfilled — contributes 0
                Track(cut, duration: 120d),    // Cut track must not count toward mix runtime
            });

        var stats = await CreateRepository().GetHomeStatsAsync();

        Assert.That(stats.MixReleaseCount, Is.EqualTo(2));
        Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(600d));
    }

    // Soft-deleted releases and tracks never count toward any figure.
    [Test]
    public async Task GetHomeStatsAsync_ExcludesSoftDeletedRowsFromAllFigures()
    {
        var liveCut = Release("Live Cut", ReleaseMedium.Cut, ReleaseType.Album);
        var deletedCut = Release("Dead Cut", ReleaseMedium.Cut, ReleaseType.Album);
        deletedCut.IsDeleted = true;
        var mix = Release("Mix", ReleaseMedium.Mix);

        var deletedMixTrack = Track(mix, duration: 999d);
        deletedMixTrack.IsDeleted = true;

        await SeedAsync(
            new[] { liveCut, deletedCut, mix },
            new[] { Track(liveCut), deletedMixTrack });

        var stats = await CreateRepository().GetHomeStatsAsync();

        Assert.That(stats.CutTrackCount, Is.EqualTo(1));
        Assert.That(stats.CutReleaseTypeCounts.Single(c => c.ReleaseType == ReleaseType.Album).Count, Is.EqualTo(1));
        Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d), "the only mix track is soft-deleted");
    }

    // A live track under a directly-deleted release must be excluded from the track-based figures.
    // SoftDeleteReleaseAsync does not cascade to child tracks, so without the !t.Release.IsDeleted
    // guard the track-count and runtime figures are internally inconsistent with the release-level ones.
    [Test]
    public async Task GetHomeStatsAsync_ExcludesLiveTracksUnderSoftDeletedRelease()
    {
        var liveCut = Release("Live Cut", ReleaseMedium.Cut, ReleaseType.Album);
        var deletedCut = Release("Dead Cut", ReleaseMedium.Cut, ReleaseType.Album);
        deletedCut.IsDeleted = true;
        var deletedMix = Release("Dead Mix", ReleaseMedium.Mix);
        deletedMix.IsDeleted = true;

        // Both tracks are themselves live — only their parent release is soft-deleted.
        var liveTrackUnderDeletedCut = Track(deletedCut);
        var liveTrackUnderDeletedMix = Track(deletedMix, duration: 900d);

        await SeedAsync(
            new[] { liveCut, deletedCut, deletedMix },
            new[] { Track(liveCut), liveTrackUnderDeletedCut, liveTrackUnderDeletedMix });

        var stats = await CreateRepository().GetHomeStatsAsync();

        Assert.That(stats.CutTrackCount, Is.EqualTo(1),
            "live track under deleted Cut release must not inflate CutTrackCount");
        Assert.That(stats.MixReleaseCount, Is.EqualTo(0),
            "deleted Mix release must not count");
        Assert.That(stats.MixRuntimeSeconds, Is.EqualTo(0d),
            "live track under deleted Mix release must not inflate MixRuntimeSeconds");
    }
}
