using DeepDrftData.Data;
using DeepDrftData.Repositories;
using DeepDrftModels.Entities;
using DeepDrftModels.Enums;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace DeepDrftTests;

/// <summary>
/// Storage-layer tests for the Phase 16 telemetry writes (<see cref="EventRepository"/>): server-side
/// release resolution (§2.3 / D4), the incremental play-counter bump in the same write (D6), the
/// derived-release-total shape (a release's plays are the sum of its tracks'), and the share append.
/// Runs on the EF in-memory provider like <see cref="HomeStatsQueryTests"/>. In-memory does not support
/// real transactions, so the transaction-ignored warning is suppressed — the production Postgres path
/// wraps the append + bump in one transaction, which the warning would otherwise turn into an error here.
/// </summary>
[TestFixture]
public class PlayEventQueryTests
{
    private DeepDrftContext _context = null!;

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

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

    private EventRepository CreateRepository() => new(_context);

    private async Task<(ReleaseEntity Release, TrackEntity Track)> SeedTrackAsync(string trackKey)
    {
        var release = new ReleaseEntity
        {
            EntryKey = Guid.NewGuid().ToString("N"),
            Title = "R",
            Artist = "A",
            Medium = ReleaseMedium.Cut,
        };
        var track = new TrackEntity { EntryKey = trackKey, TrackName = "T", Release = release };
        _context.Releases.Add(release);
        _context.Tracks.Add(track);
        await _context.SaveChangesAsync();
        return (release, track);
    }

    // A play that reaches the repository (the floor is the tracker's job) writes exactly one play_event
    // row with the release id resolved server-side, and bumps the matching bucket on the track's counter.
    [Test]
    public async Task RecordPlayAsync_ResolvesReleaseAndBumpsCounter()
    {
        var (release, track) = await SeedTrackAsync("track-1");

        await CreateRepository().RecordPlayAsync("track-1", PlayBucket.Complete, anonId: null);

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.TrackEntryKey, Is.EqualTo("track-1"));
        Assert.That(ev.ReleaseId, Is.EqualTo(release.Id), "release resolved server-side from the track key");
        Assert.That(ev.Bucket, Is.EqualTo(PlayBucket.Complete));
        Assert.That(ev.AnonId, Is.Null, "no anonId is written in wave 16.1");

        var counter = await _context.PlayCounters.SingleAsync(c => c.TrackId == track.Id);
        Assert.That(counter.CompleteCount, Is.EqualTo(1));
        Assert.That(counter.TotalPlays, Is.EqualTo(1));
    }

    // Each bucket bumps its own column; total plays is the sum across buckets.
    [Test]
    public async Task RecordPlayAsync_BucketsAccumulateIndependently()
    {
        var (_, track) = await SeedTrackAsync("track-1");
        var repo = CreateRepository();

        await repo.RecordPlayAsync("track-1", PlayBucket.Partial, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Sampled, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Sampled, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);

        var counter = await _context.PlayCounters.SingleAsync(c => c.TrackId == track.Id);
        Assert.That(counter.PartialCount, Is.EqualTo(1));
        Assert.That(counter.SampledCount, Is.EqualTo(2));
        Assert.That(counter.CompleteCount, Is.EqualTo(1));
        Assert.That(counter.TotalPlays, Is.EqualTo(4));
        Assert.That(await _context.PlayEvents.CountAsync(), Is.EqualTo(4));
    }

    // Release totals are derived (D4): summing the counters of the release's tracks gives release plays;
    // there is no separate release-counter row.
    [Test]
    public async Task RecordPlayAsync_ReleaseTotalIsSumOfTrackCounters()
    {
        var release = new ReleaseEntity
        {
            EntryKey = Guid.NewGuid().ToString("N"), Title = "R", Artist = "A", Medium = ReleaseMedium.Cut,
        };
        var t1 = new TrackEntity { EntryKey = "t1", TrackName = "T1", Release = release };
        var t2 = new TrackEntity { EntryKey = "t2", TrackName = "T2", Release = release };
        _context.Releases.Add(release);
        _context.Tracks.AddRange(t1, t2);
        await _context.SaveChangesAsync();

        var repo = CreateRepository();
        await repo.RecordPlayAsync("t1", PlayBucket.Complete, null);
        await repo.RecordPlayAsync("t1", PlayBucket.Partial, null);
        await repo.RecordPlayAsync("t2", PlayBucket.Sampled, null);

        var releaseTotal = await _context.PlayCounters
            .Where(c => _context.Tracks.Any(t => t.Id == c.TrackId && t.ReleaseId == release.Id))
            .SumAsync(c => c.PartialCount + c.SampledCount + c.CompleteCount);

        Assert.That(releaseTotal, Is.EqualTo(3));
    }

    // A loose track (no release) logs the event with a null release id and still bumps its own counter.
    [Test]
    public async Task RecordPlayAsync_LooseTrack_NullReleaseStillCounts()
    {
        var track = new TrackEntity { EntryKey = "loose", TrackName = "T" };
        _context.Tracks.Add(track);
        await _context.SaveChangesAsync();

        await CreateRepository().RecordPlayAsync("loose", PlayBucket.Sampled, null);

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.ReleaseId, Is.Null);
        Assert.That((await _context.PlayCounters.SingleAsync(c => c.TrackId == track.Id)).SampledCount, Is.EqualTo(1));
    }

    // A play of an unknown/removed track key still logs (null release, no counter bump) rather than failing.
    [Test]
    public async Task RecordPlayAsync_UnknownTrackKey_LogsEventWithoutCounter()
    {
        await CreateRepository().RecordPlayAsync("does-not-exist", PlayBucket.Partial, null);

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.ReleaseId, Is.Null);
        Assert.That(await _context.PlayCounters.AnyAsync(), Is.False, "no track to roll up against");
    }

    // A soft-deleted track resolves to null (the !IsDeleted guard) — the play still logs, no counter bump.
    [Test]
    public async Task RecordPlayAsync_SoftDeletedTrack_DoesNotResolveRelease()
    {
        var (_, track) = await SeedTrackAsync("gone");
        track.IsDeleted = true;
        await _context.SaveChangesAsync();

        await CreateRepository().RecordPlayAsync("gone", PlayBucket.Complete, null);

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.ReleaseId, Is.Null);
        Assert.That(await _context.PlayCounters.AnyAsync(c => c.TrackId == track.Id), Is.False);
    }

    // --- Site-wide total plays (§5 — the home Plays card's primary figure) ---

    // An empty counter table sums to zero rather than throwing — the card's reading until the
    // telemetry migration is applied and the first play lands.
    [Test]
    public async Task CountTotalPlaysAsync_NoCounters_IsZero()
    {
        Assert.That(await CreateRepository().CountTotalPlaysAsync(), Is.EqualTo(0L));
    }

    // Total plays sums all three bucket columns of a single track's counter.
    [Test]
    public async Task CountTotalPlaysAsync_SumsAllBucketsOfOneCounter()
    {
        await SeedTrackAsync("track-1");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("track-1", PlayBucket.Partial, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Sampled, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Sampled, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);

        Assert.That(await repo.CountTotalPlaysAsync(), Is.EqualTo(4L));
    }

    // Total plays is site-wide: it sums across every track's counter, not one track's.
    [Test]
    public async Task CountTotalPlaysAsync_SumsAcrossAllTracks()
    {
        await SeedTrackAsync("track-1");
        await SeedTrackAsync("track-2");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Partial, null);
        await repo.RecordPlayAsync("track-2", PlayBucket.Sampled, null);

        Assert.That(await repo.CountTotalPlaysAsync(), Is.EqualTo(3L),
            "site-wide total spans every track's counter");
    }

    // A share append writes one row with the target, channel, and a null anonId.
    [Test]
    public async Task RecordShareAsync_AppendsRow()
    {
        await CreateRepository().RecordShareAsync(ShareTargetType.Release, "rel-key", ShareChannel.Embed, anonId: null);

        var ev = await _context.ShareEvents.SingleAsync();
        Assert.That(ev.TargetType, Is.EqualTo(ShareTargetType.Release));
        Assert.That(ev.TargetKey, Is.EqualTo("rel-key"));
        Assert.That(ev.Channel, Is.EqualTo(ShareChannel.Embed));
        Assert.That(ev.AnonId, Is.Null);
    }
}
