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 wave-16.3 anon-id layer (<see cref="EventRepository"/>): the
/// anon id persists to the <c>anon_id</c> column on play and share writes (and a null persists null),
/// and the all-time distinct-listener aggregation (§3 / D3) is correct site-wide, per-track, and
/// per-release (derived), with null anon ids excluded from every distinct count. Runs on the EF
/// in-memory provider like <see cref="PlayEventQueryTests"/>; the transaction-ignored warning is
/// suppressed because in-memory has no real transactions (the play write wraps append + bump in one).
/// </summary>
[TestFixture]
public class AnonIdQueryTests
{
    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);
    }

    // --- Persistence of the anon id ---

    // A play carrying an anon id writes it to the column.
    [Test]
    public async Task RecordPlayAsync_WithAnonId_PersistsIt()
    {
        await SeedTrackAsync("track-1");

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

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.AnonId, Is.EqualTo("anon-abc"));
    }

    // A play with no anon id (the provider returned null) persists a null column — both paths covered.
    [Test]
    public async Task RecordPlayAsync_WithoutAnonId_PersistsNull()
    {
        await SeedTrackAsync("track-1");

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

        var ev = await _context.PlayEvents.SingleAsync();
        Assert.That(ev.AnonId, Is.Null);
    }

    // A share carrying an anon id writes it to the column; a null share persists null.
    [Test]
    public async Task RecordShareAsync_PersistsAnonId()
    {
        var repo = CreateRepository();
        await repo.RecordShareAsync(ShareTargetType.Track, "k", ShareChannel.Link, anonId: "anon-xyz");
        await repo.RecordShareAsync(ShareTargetType.Track, "k", ShareChannel.Embed, anonId: null);

        var withId = await _context.ShareEvents.SingleAsync(e => e.Channel == ShareChannel.Link);
        var without = await _context.ShareEvents.SingleAsync(e => e.Channel == ShareChannel.Embed);
        Assert.That(withId.AnonId, Is.EqualTo("anon-xyz"));
        Assert.That(without.AnonId, Is.Null);
    }

    // --- Site-wide distinct listeners (§3 / D3, all-time) ---

    // Distinct anon ids are counted once each; a listener who plays many times counts once.
    [Test]
    public async Task CountDistinctListeners_CountsEachAnonOnce()
    {
        await SeedTrackAsync("track-1");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, "anon-1");
        await repo.RecordPlayAsync("track-1", PlayBucket.Partial, "anon-1");   // same listener, replay
        await repo.RecordPlayAsync("track-1", PlayBucket.Sampled, "anon-2");

        Assert.That(await repo.CountDistinctListenersAsync(), Is.EqualTo(2));
    }

    // Null anon ids are excluded from the distinct count — an anonId-less play is not a known listener.
    [Test]
    public async Task CountDistinctListeners_ExcludesNullAnonIds()
    {
        await SeedTrackAsync("track-1");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, "anon-1");
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);

        Assert.That(await repo.CountDistinctListenersAsync(), Is.EqualTo(1),
            "null anonIds must not inflate the listener count");
    }

    // With no anon ids at all, the count is zero (not an error).
    [Test]
    public async Task CountDistinctListeners_AllNull_IsZero()
    {
        await SeedTrackAsync("track-1");
        await CreateRepository().RecordPlayAsync("track-1", PlayBucket.Complete, null);

        Assert.That(await CreateRepository().CountDistinctListenersAsync(), Is.EqualTo(0));
    }

    // --- Per-track distinct listeners ---

    // The per-track count scopes to the track key and counts distinct non-null anon ids.
    [Test]
    public async Task CountDistinctListenersForTrack_ScopesToTrack()
    {
        await SeedTrackAsync("track-1");
        await SeedTrackAsync("track-2");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, "anon-1");
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, "anon-2");
        await repo.RecordPlayAsync("track-1", PlayBucket.Complete, null);   // excluded
        await repo.RecordPlayAsync("track-2", PlayBucket.Complete, "anon-3");

        Assert.That(await repo.CountDistinctListenersForTrackAsync("track-1"), Is.EqualTo(2));
        Assert.That(await repo.CountDistinctListenersForTrackAsync("track-2"), Is.EqualTo(1));
    }

    // --- Per-release distinct listeners (derived, D4) ---

    // A release's listener count is the distinct anon ids across all its tracks: a listener who heard two
    // tracks of the release counts once (union, not a per-track sum), and null anon ids are excluded.
    [Test]
    public async Task CountDistinctListenersForRelease_DistinctAcrossTracks()
    {
        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, "anon-1");
        await repo.RecordPlayAsync("t2", PlayBucket.Complete, "anon-1");   // same listener, second track
        await repo.RecordPlayAsync("t2", PlayBucket.Complete, "anon-2");
        await repo.RecordPlayAsync("t1", PlayBucket.Complete, null);       // excluded

        Assert.That(await repo.CountDistinctListenersForReleaseAsync(release.Id), Is.EqualTo(2),
            "anon-1 heard two tracks but is one distinct listener of the release");
    }

    // A play of a track in another release does not bleed into this release's listener count.
    [Test]
    public async Task CountDistinctListenersForRelease_ExcludesOtherReleases()
    {
        var (releaseA, _) = await SeedTrackAsync("a-track");
        var (releaseB, _) = await SeedTrackAsync("b-track");
        var repo = CreateRepository();
        await repo.RecordPlayAsync("a-track", PlayBucket.Complete, "anon-1");
        await repo.RecordPlayAsync("b-track", PlayBucket.Complete, "anon-2");

        Assert.That(await repo.CountDistinctListenersForReleaseAsync(releaseA.Id), Is.EqualTo(1));
        Assert.That(await repo.CountDistinctListenersForReleaseAsync(releaseB.Id), Is.EqualTo(1));
    }
}
