using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.ProgramSchedules;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Scheduling;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Application.ProgramSchedules;

/// <summary>
///     Regression tests for #229. The write path (Replace = PUT, Add = POST) used to project freshly-persisted
///     items whose watermark/graphics join rows only carry foreign-key ids — the Watermark / GraphicsElement navs
///     are null, and <c>Mapper.ProjectToViewModel</c> dereferences them without a guard, throwing an NRE that
///     surfaced as a 500. The pre-existing round-trip test masked this because the Replace handler returned a
///     <b>lazy</b> LanguageExt <c>Map</c> sequence and that test only checked <c>.IsRight</c> — it never enumerated
///     the projected items, so the deferred NRE never fired. These tests force enumeration of the write-path
///     response (exactly as the controller's <c>.ToList()</c> / single-item serialization does) and seed the
///     watermark/graphics entities through a <b>separate</b> context so the handler's fresh factory context has
///     nothing pre-tracked (no accidental change-tracker fix-up).
/// </summary>
[TestFixture]
public class ScheduleItemWriteProjectionTests
{
    private InMemoryTvContext _db = null!;
    private ChannelWriter<IBackgroundServiceRequest> _worker = null!;

    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _worker = System.Threading.Channels.Channel.CreateUnbounded<IBackgroundServiceRequest>().Writer;
    }

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

    [Test]
    public async Task Replace_PutResponse_Should_Carry_Watermark_And_Graphics_Names()
    {
        int scheduleId = await SeedReferences();

        var handler = new ReplaceProgramScheduleItemsHandler(_db.Factory, _worker);
        Either<BaseError, IEnumerable<ProgramScheduleItemViewModel>> result = await handler.Handle(
            new ReplaceProgramScheduleItems(scheduleId, [ItemWithGraphics(0)]),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue(result.LeftToSeq().HeadOrNone().Match(e => e.Value, () => "unknown"));

        // Force enumeration exactly like the controller (`items.Select(...).ToList()`) — this is what threw the NRE.
        List<ProgramScheduleItemViewModel> items = result.RightToSeq().Head().ToList();
        ProgramScheduleItemViewModel item = items.Single();
        item.Watermarks.Select(w => w.Name).ShouldBe(["WM One", "WM Two"]);
        item.GraphicsElements.Count.ShouldBe(2);
        item.GraphicsElements.Select(g => g.Name).ShouldAllBe(n => !string.IsNullOrWhiteSpace(n));
    }

    [Test]
    public async Task Add_PostResponse_Should_Carry_Watermark_And_Graphics_Names()
    {
        int scheduleId = await SeedReferences();

        var handler = new AddProgramScheduleItemHandler(_db.Factory, _worker);
        Either<BaseError, ProgramScheduleItemViewModel> result = await handler.Handle(
            AddItemWithGraphics(scheduleId),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue(result.LeftToSeq().HeadOrNone().Match(e => e.Value, () => "unknown"));

        ProgramScheduleItemViewModel item = result.RightToSeq().Head();
        item.Watermarks.Select(w => w.Name).ShouldBe(["WM One", "WM Two"]);
        item.GraphicsElements.Count.ShouldBe(2);
    }

    [Test]
    public async Task Get_Should_Return_Items_In_Index_Order_Regardless_Of_Id_Order()
    {
        int scheduleId;
        await using (TvContext ctx = _db.CreateContext())
        {
            var schedule = new ProgramSchedule
            {
                Name = "Shuffled", Items = [], Playouts = [], ProgramScheduleAlternates = []
            };
            ctx.ProgramSchedules.Add(schedule);
            await ctx.SaveChangesAsync();
            scheduleId = schedule.Id;

            // Insert with ids deliberately out of Index order: id 10 -> Index 2, id 11 -> Index 0, id 12 -> Index 1.
            // Before the fix the GET returned id order (2, 0, 1); after, it must return Index order (0, 1, 2).
            ctx.ProgramScheduleItems.Add(OneItemRow(10, scheduleId, index: 2));
            ctx.ProgramScheduleItems.Add(OneItemRow(11, scheduleId, index: 0));
            ctx.ProgramScheduleItems.Add(OneItemRow(12, scheduleId, index: 1));
            await ctx.SaveChangesAsync();
        }

        var getHandler = new GetProgramScheduleItemsHandler(_db.Factory);
        List<ProgramScheduleItemViewModel> items =
            await getHandler.Handle(new GetProgramScheduleItems(scheduleId), CancellationToken.None);

        items.Select(i => i.Index).ShouldBe([0, 1, 2]);
    }

    private static ProgramScheduleItemOne OneItemRow(int id, int scheduleId, int index) =>
        new()
        {
            Id = id,
            ProgramScheduleId = scheduleId,
            Index = index,
            CollectionType = CollectionType.Collection,
            CollectionId = 1,
            PlaybackOrder = PlaybackOrder.Shuffle,
            ProgramScheduleItemWatermarks = [],
            ProgramScheduleItemGraphicsElements = []
        };

    private async Task<int> SeedReferences()
    {
        await using TvContext ctx = _db.CreateContext();
        ctx.Collections.Add(new Collection { Id = 1, Name = "First Collection" });
        ctx.ChannelWatermarks.Add(new ChannelWatermark { Id = 1, Name = "WM One" });
        ctx.ChannelWatermarks.Add(new ChannelWatermark { Id = 2, Name = "WM Two" });
        ctx.GraphicsElements.Add(new GraphicsElement { Id = 1, Name = "GE One", Path = "/ge/1", Kind = GraphicsElementKind.Image });
        ctx.GraphicsElements.Add(new GraphicsElement { Id = 2, Name = "GE Two", Path = "/ge/2", Kind = GraphicsElementKind.Image });

        var schedule = new ProgramSchedule
        {
            Name = "WM Schedule", Items = [], Playouts = [], ProgramScheduleAlternates = []
        };
        ctx.ProgramSchedules.Add(schedule);
        await ctx.SaveChangesAsync();
        return schedule.Id;
    }

    private static ReplaceProgramScheduleItem ItemWithGraphics(int index) =>
        new(
            null, index, StartType.Dynamic, StartTime: null, FixedStartTimeBehavior: null, PlayoutMode.One,
            CollectionType.Collection, CollectionId: 1, MultiCollectionId: null, SmartCollectionId: null,
            RerunCollectionId: null, MediaItemId: null, PlaylistId: null, SearchTitle: null, SearchQuery: null,
            PlaybackOrder: PlaybackOrder.Shuffle, MarathonGroupBy: MarathonGroupBy.None,
            MarathonShuffleGroups: false, MarathonShuffleItems: false, MarathonBatchSize: null,
            FillWithGroupMode: FillWithGroupMode.None, MultipleMode: MultipleMode.Count, MultipleCount: null,
            PlayoutDuration: null, TailMode: TailMode.None, DiscardToFillAttempts: null, CustomTitle: null,
            GuideMode: GuideMode.Normal, PreRollFillerId: null, MidRollFillerId: null, PostRollFillerId: null,
            TailFillerId: null, FallbackFillerId: null, WatermarkIds: [1, 2], GraphicsElementIds: [1, 2],
            PreferredAudioLanguageCode: null, PreferredAudioTitle: null, PreferredSubtitleLanguageCode: null,
            SubtitleMode: null);

    private static AddProgramScheduleItem AddItemWithGraphics(int scheduleId) =>
        new(
            scheduleId, StartType.Dynamic, StartTime: null, FixedStartTimeBehavior: null, PlayoutMode.One,
            CollectionType.Collection, CollectionId: 1, MultiCollectionId: null, SmartCollectionId: null,
            RerunCollectionId: null, MediaItemId: null, PlaylistId: null, SearchTitle: null, SearchQuery: null,
            PlaybackOrder: PlaybackOrder.Shuffle, MarathonGroupBy: MarathonGroupBy.None,
            MarathonShuffleGroups: false, MarathonShuffleItems: false, MarathonBatchSize: null,
            FillWithGroupMode: FillWithGroupMode.None, MultipleMode: MultipleMode.Count, MultipleCount: null,
            PlayoutDuration: null, TailMode: TailMode.None, DiscardToFillAttempts: null, CustomTitle: null,
            GuideMode: GuideMode.Normal, PreRollFillerId: null, MidRollFillerId: null, PostRollFillerId: null,
            TailFillerId: null, FallbackFillerId: null, WatermarkIds: [1, 2], GraphicsElementIds: [1, 2],
            PreferredAudioLanguageCode: null, PreferredAudioTitle: null, PreferredSubtitleLanguageCode: null,
            SubtitleMode: null);
}
