using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Application.Scheduling;

/// <summary>
///     Regression tests for issue #172 (API hardening).
///     FIX A: ten command handlers guarded a client-nullable <c>Name</c> with a bare
///     <c>request.Name.Length > 50</c>, so a null/whitespace name threw a NullReferenceException
///     that surfaced as HTTP 500. The guard is now
///     <c>string.IsNullOrWhiteSpace(request.Name) || request.Name.Length > 50</c>, which returns a
///     validation failure (Left) instead of throwing.
///     FIX B: <see cref="ReplaceTemplateItemsHandler" /> overlap validation used to skip
///     value-equal items (<c>item == otherItem</c> on a record), which let two exact-duplicate
///     items bypass the intersection check and both persist. Validation now iterates by index, so
///     duplicates register as a self-intersection and are rejected.
/// </summary>
[TestFixture]
public class Issue172HardeningTests
{
    private InMemoryTvContext _db = null!;

    [SetUp]
    public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();

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

    private static bool IsLeft<T>(Either<BaseError, T> result) => result.Match(Right: _ => false, Left: _ => true);

    // ---- FIX B: duplicate template items must be rejected -------------------------------------

    [Test]
    public async Task ReplaceTemplateItems_Should_Reject_Duplicate_Items()
    {
        // Block Minutes=30 gives each item a non-zero duration (0:00..0:30) so the two identical
        // items overlap. Under the OLD value-equality skip the two records were `==` and skipped,
        // letting both persist unvalidated; the index-based check now flags the self-intersection.
        await using (TvContext seed = _db.CreateContext())
        {
            seed.Blocks.Add(
                new Block
                {
                    Id = 10,
                    BlockGroupId = 1,
                    Name = "Morning",
                    Minutes = 30,
                    StopScheduling = BlockStopScheduling.AfterDurationEnd,
                    Items = new List<BlockItem>()
                });
            seed.Templates.Add(
                new Template
                {
                    Id = 1,
                    TemplateGroupId = 1,
                    Name = "Weekday",
                    Version = 1,
                    Items = new List<TemplateItem>()
                });
            await seed.SaveChangesAsync();
        }

        var handler = new ReplaceTemplateItemsHandler(_db.Factory);

        var duplicate = new ReplaceTemplateItem(10, TimeSpan.Zero);
        var request = new ReplaceTemplateItems(
            1,
            1,
            "Weekday",
            new List<ReplaceTemplateItem> { duplicate, duplicate },
            ExpectedVersions: default); // None -> force-write path

        Either<BaseError, List<TemplateItemViewModel>> result =
            await handler.Handle(request, CancellationToken.None);

        IsLeft(result).ShouldBeTrue();

        // Non-vacuous: nothing was persisted (the old code would have written both items).
        await using TvContext ctx = _db.CreateContext();
        (await ctx.TemplateItems.CountAsync(i => i.TemplateId == 1)).ShouldBe(0);
    }

    // ---- FIX A: null/whitespace name returns Left instead of throwing -------------------------

    [TestCase(null)]
    [TestCase("   ")]
    public async Task CreateBlock_Should_Reject_Blank_Name(string? name)
    {
        await SeedGroupAsync(g => g.BlockGroups.Add(new BlockGroup { Id = 1, Name = "G", Blocks = new List<Block>() }));

        var handler = new CreateBlockHandler(_db.Factory);
        Either<BaseError, BlockViewModel> result =
            await handler.Handle(new CreateBlock(1, name!), CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task CreateTemplate_Should_Reject_Blank_Name(string? name)
    {
        await SeedGroupAsync(g =>
            g.TemplateGroups.Add(new TemplateGroup { Id = 1, Name = "G", Templates = new List<Template>() }));

        var handler = new CreateTemplateHandler(_db.Factory);
        Either<BaseError, TemplateViewModel> result =
            await handler.Handle(new CreateTemplate(1, name!), CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task CreateDeco_Should_Reject_Blank_Name(string? name)
    {
        await SeedGroupAsync(g => g.DecoGroups.Add(new DecoGroup { Id = 1, Name = "G", Decos = new List<Deco>() }));

        var handler = new CreateDecoHandler(_db.Factory);
        Either<BaseError, DecoViewModel> result =
            await handler.Handle(new CreateDeco(1, name!), CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task CreateDecoTemplate_Should_Reject_Blank_Name(string? name)
    {
        await SeedGroupAsync(g => g.DecoTemplateGroups.Add(
            new DecoTemplateGroup { Id = 1, Name = "G", DecoTemplates = new List<DecoTemplate>() }));

        var handler = new CreateDecoTemplateHandler(_db.Factory);
        Either<BaseError, DecoTemplateViewModel> result =
            await handler.Handle(new CreateDecoTemplate(1, name!), CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("")]
    public async Task CreatePlaylist_Should_Reject_Blank_Name(string? name)
    {
        // The DTO coalesces null -> "" in production; the handler guard rejects both empty and null.
        var handler = new CreatePlaylistHandler(_db.Factory);
        Either<BaseError, PlaylistViewModel> result =
            await handler.Handle(new CreatePlaylist(1, name!), CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task ReplaceBlockItems_Should_Reject_Blank_Name(string? name)
    {
        await using (TvContext seed = _db.CreateContext())
        {
            seed.Blocks.Add(
                new Block
                {
                    Id = 1,
                    BlockGroupId = 1,
                    Name = "X",
                    Minutes = 30,
                    StopScheduling = BlockStopScheduling.AfterDurationEnd,
                    Items = new List<BlockItem>()
                });
            await seed.SaveChangesAsync();
        }

        var handler = new ReplaceBlockItemsHandler(_db.Factory);
        var request = new ReplaceBlockItems(
            1,
            1,
            name!,
            30,
            BlockStopScheduling.AfterDurationEnd,
            new List<ReplaceBlockItem>());

        Either<BaseError, Unit> result = await handler.Handle(request, CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task ReplaceTemplateItems_Should_Reject_Blank_Name(string? name)
    {
        await using (TvContext seed = _db.CreateContext())
        {
            seed.Templates.Add(
                new Template
                {
                    Id = 1,
                    TemplateGroupId = 1,
                    Name = "Weekday",
                    Version = 1,
                    Items = new List<TemplateItem>()
                });
            await seed.SaveChangesAsync();
        }

        var handler = new ReplaceTemplateItemsHandler(_db.Factory);
        var request = new ReplaceTemplateItems(
            1,
            1,
            name!,
            new List<ReplaceTemplateItem>());

        Either<BaseError, List<TemplateItemViewModel>> result =
            await handler.Handle(request, CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task ReplaceDecoTemplateItems_Should_Reject_Blank_Name(string? name)
    {
        await using (TvContext seed = _db.CreateContext())
        {
            seed.DecoTemplates.Add(
                new DecoTemplate
                {
                    Id = 1,
                    DecoTemplateGroupId = 1,
                    Name = "X",
                    Items = new List<DecoTemplateItem>()
                });
            await seed.SaveChangesAsync();
        }

        var channel = Substitute.For<ChannelWriter<IBackgroundServiceRequest>>();
        var handler = new ReplaceDecoTemplateItemsHandler(_db.Factory, channel);
        var request = new ReplaceDecoTemplateItems(
            1,
            1,
            name!,
            new List<ReplaceDecoTemplateItem>());

        Either<BaseError, List<DecoTemplateItemViewModel>> result =
            await handler.Handle(request, CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    [TestCase(null)]
    [TestCase("   ")]
    public async Task UpdateDeco_Should_Reject_Blank_Name(string? name)
    {
        await using (TvContext seed = _db.CreateContext())
        {
            seed.Decos.Add(
                new Deco
                {
                    Id = 1,
                    DecoGroupId = 1,
                    Name = "X",
                    BreakContent = [],
                    DecoWatermarks = [],
                    DecoGraphicsElements = []
                });
            await seed.SaveChangesAsync();
        }

        var channel = Substitute.For<ChannelWriter<IBackgroundServiceRequest>>();
        var handler = new UpdateDecoHandler(_db.Factory, channel);
        var request = new UpdateDeco(
            1,
            1,
            name!,
            DecoMode.Inherit,
            new List<int>(),
            false,
            DecoMode.Inherit,
            new List<int>(),
            false,
            DecoMode.Inherit,
            new List<UpdateDecoBreakContent>(),
            DecoMode.Inherit,
            CollectionType.Collection,
            null,
            null,
            null,
            null,
            false,
            DecoMode.Inherit,
            CollectionType.Collection,
            null,
            null,
            null,
            null);

        Either<BaseError, Unit> result = await handler.Handle(request, CancellationToken.None);

        IsLeft(result).ShouldBeTrue();
    }

    private async Task SeedGroupAsync(Action<TvContext> seed)
    {
        await using TvContext ctx = _db.CreateContext();
        seed(ctx);
        await ctx.SaveChangesAsync();
    }
}
