using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using ErsatzTV.Application;
using ErsatzTV.Application.Playouts;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using MediatR;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Channel = System.Threading.Channels.Channel;

namespace ErsatzTV.Tests.Application.Playouts;

[TestFixture]
public class ReshufflePlayoutHandlerTests
{
    private InMemoryTvContext _db = null!;
    private Channel<IBackgroundServiceRequest> _worker = null!;
    private IMediator _mediator = null!;

    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _worker = Channel.CreateUnbounded<IBackgroundServiceRequest>();
        _mediator = Substitute.For<IMediator>();
    }

    [TearDown]
    public async Task TearDown() => await _db.DisposeAsync();

    private ReshufflePlayoutHandler CreateHandler() => new(_mediator, _worker.Writer, _db.Factory);

    private async Task<int> SeedPlayout(PlayoutScheduleKind kind, int? seed = null)
    {
        await using TvContext context = _db.CreateContext();
        var playout = new Playout { ChannelId = 0, ScheduleKind = kind };
        if (seed.HasValue)
        {
            playout.Seed = seed.Value;
        }

        context.Playouts.Add(playout);
        await context.SaveChangesAsync();
        return playout.Id;
    }

    [TestCase(PlayoutScheduleKind.Classic)]
    [TestCase(PlayoutScheduleKind.Block)]
    [TestCase(PlayoutScheduleKind.Sequential)]
    [TestCase(PlayoutScheduleKind.Scripted)]
    public async Task Handle_Should_Erase_History_Then_Enqueue_Reset_Build_For_Supported_Kind(
        PlayoutScheduleKind kind)
    {
        int id = await SeedPlayout(kind);

        await CreateHandler().Handle(new ReshufflePlayout(id), CancellationToken.None);

        await _mediator.Received(1).Send(
            Arg.Is<ErasePlayoutHistory>(e => e.PlayoutId == id),
            Arg.Any<CancellationToken>());

        _worker.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
        var build = request.ShouldBeOfType<BuildPlayout>();
        build.PlayoutId.ShouldBe(id);
        build.Mode.ShouldBe(PlayoutBuildMode.Reset);
        _worker.Reader.TryRead(out _).ShouldBeFalse();
    }

    [TestCase(PlayoutScheduleKind.ExternalJson)]
    [TestCase(PlayoutScheduleKind.None)]
    public async Task Handle_Should_Not_Erase_History_Or_Enqueue_For_Unsupported_Kind(PlayoutScheduleKind kind)
    {
        int id = await SeedPlayout(kind);

        await CreateHandler().Handle(new ReshufflePlayout(id), CancellationToken.None);

        await _mediator.DidNotReceive().Send(Arg.Any<ErasePlayoutHistory>(), Arg.Any<CancellationToken>());
        _worker.Reader.TryRead(out _).ShouldBeFalse();
    }

    [Test]
    public async Task Handle_Should_Actually_Reseed_Block_Playout_Via_ErasePlayoutHistory()
    {
        // This is the C1 regression test: BuildPlayout(Reset) alone only reseeds Playout.Seed for
        // Classic playouts (PlayoutBuilder). For Block/Sequential/Scripted it is a no-op reshuffle
        // unless the handler routes through ErasePlayoutHistory first. Wire the substituted
        // IMediator to actually invoke the real ErasePlayoutHistoryHandler against the in-memory DB
        // so the reseed side effect is genuinely exercised, not merely asserted-as-called.
        const int originalSeed = 12345;
        int id = await SeedPlayout(PlayoutScheduleKind.Block, originalSeed);

        // seed a PlayoutHistory row so we can also assert the deterministic "cleared" side effect
        // (avoids relying solely on new Random().Next() != originalSeed, which is a ~1-in-2^31 flake)
        await using (TvContext seedContext = _db.CreateContext())
        {
            seedContext.PlayoutHistory.Add(
                new PlayoutHistory
                {
                    PlayoutId = id,
                    Key = "test-collection",
                    When = DateTime.UtcNow,
                    Finish = DateTime.UtcNow
                });
            await seedContext.SaveChangesAsync();
        }

        _mediator.Send(Arg.Any<ErasePlayoutHistory>(), Arg.Any<CancellationToken>())
            .Returns(callInfo => new ErasePlayoutHistoryHandler(_db.Factory).Handle(
                (ErasePlayoutHistory)callInfo[0],
                (CancellationToken)callInfo[1]));

        await CreateHandler().Handle(new ReshufflePlayout(id), CancellationToken.None);

        await using TvContext context = _db.CreateContext();
        Playout playout = await context.Playouts.SingleAsync(p => p.Id == id);
        playout.Seed.ShouldNotBe(originalSeed);

        List<PlayoutHistory> remainingHistory = await context.PlayoutHistory
            .Where(h => h.PlayoutId == id)
            .ToListAsync();
        remainingHistory.ShouldBeEmpty();
    }
}
