using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Domain;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Tests.Support;
using LanguageExt;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Infrastructure;

// #72: the channels API reports a per-channel playout count so the SPA can flag a channel that will
// never play. GetChannel omitted the Playouts include, and the read is AsNoTracking with no lazy-loading
// proxies, so the navigation came back empty and every caller's count collapsed to 0 — GET
// /api/v1/channels/{id} reported playoutCount: 0 for every channel on the system. That silently disabled
// the channel editor's playout-source guard (ChannelEditScreen "Cannot be changed once a generated
// channel has a playout"), which is gated on playoutCount > 0.
//
// These run the REAL repository against a real context on purpose: a handler test with a substituted
// IChannelRepository hands the mapper a channel whose Playouts the test itself populated, so it passes
// whether or not the query includes them. Only a repository-level read can catch a missing include.
[TestFixture]
public class ChannelRepositoryPlayoutIncludeTests
{
    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _repository = new ChannelRepository(_db.Factory);
    }

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

    private InMemoryTvContext _db = null!;
    private ChannelRepository _repository = null!;

    // FFmpegProfileId is non-nullable, so GetChannel's Include(c => c.FFmpegProfile) is an INNER JOIN —
    // a channel with no profile row is filtered out of the result entirely. Every fixture needs a real one.
    private async Task<int> SeedFFmpegProfile()
    {
        var profile = new FFmpegProfile { Name = "Test Profile" };
        await using TvContext context = _db.CreateContext();
        context.FFmpegProfiles.Add(profile);
        await context.SaveChangesAsync();
        return profile.Id;
    }

    private async Task<int> SeedChannel(Channel channel)
    {
        await using TvContext context = _db.CreateContext();
        context.Channels.Add(channel);
        await context.SaveChangesAsync();
        return channel.Id;
    }

    private async Task<ChannelDetailResponseModel> GetDetail(int channelId)
    {
        var handler = new GetChannelByIdForApiHandler(_repository);
        Option<ChannelDetailResponseModel> result =
            await handler.Handle(new GetChannelByIdForApi(channelId), CancellationToken.None);
        return result.IfNone(() => throw new InvalidOperationException($"channel {channelId} not found"));
    }

    [Test]
    public async Task GetChannel_Should_Include_Playouts_So_The_Detail_Api_Reports_A_Real_Count()
    {
        int profileId = await SeedFFmpegProfile();
        int channelId = await SeedChannel(
            new Channel(Guid.NewGuid())
            {
                Number = "1",
                Name = "Generated",
                PlayoutSource = ChannelPlayoutSource.Generated,
                FFmpegProfileId = profileId,
                Artwork = [],
                Playouts = [new Playout(), new Playout()]
            });

        ChannelDetailResponseModel detail = await GetDetail(channelId);

        detail.PlayoutCount.ShouldBe(2);
    }

    [Test]
    public async Task GetChannel_Should_Count_The_Mirror_Sources_Playouts_For_A_Mirror_Channel()
    {
        // A mirror channel owns no playouts; it relays its source's. Counting only its own navigation
        // would report 0 and wrongly flag a working mirror channel as "will never play".
        int profileId = await SeedFFmpegProfile();
        int sourceId = await SeedChannel(
            new Channel(Guid.NewGuid())
            {
                Number = "1",
                Name = "Source",
                PlayoutSource = ChannelPlayoutSource.Generated,
                FFmpegProfileId = profileId,
                Artwork = [],
                Playouts = [new Playout()]
            });

        int mirrorId = await SeedChannel(
            new Channel(Guid.NewGuid())
            {
                Number = "2",
                Name = "Mirror",
                PlayoutSource = ChannelPlayoutSource.Mirror,
                MirrorSourceChannelId = sourceId,
                FFmpegProfileId = profileId,
                Artwork = [],
                Playouts = []
            });

        ChannelDetailResponseModel detail = await GetDetail(mirrorId);

        detail.PlayoutCount.ShouldBe(1);
    }

    [Test]
    public async Task GetChannel_Should_Report_Zero_For_A_Channel_With_No_Playouts()
    {
        // Boundary check on the mapper only — this one passes with or WITHOUT the Playouts include
        // (0 == 0 either way), so unlike its two siblings it is NOT include coverage. It earns its
        // place by pinning that the "will never play" signal stays 0 and never drifts vacuously
        // non-zero, which would silently retire the badge.
        int profileId = await SeedFFmpegProfile();
        int channelId = await SeedChannel(
            new Channel(Guid.NewGuid())
            {
                Number = "3",
                Name = "Empty",
                PlayoutSource = ChannelPlayoutSource.Generated,
                FFmpegProfileId = profileId,
                Artwork = [],
                Playouts = []
            });

        ChannelDetailResponseModel detail = await GetDetail(channelId);

        detail.PlayoutCount.ShouldBe(0);
    }
}
