using ErsatzTV.Application;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain.Scheduling;
using ErsatzTV.Core.Errors;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;

namespace ErsatzTV.Tests.Application.Scheduling;

/// <summary>
///     Contract tests for the #253 optimistic-concurrency mechanic on the Template aggregate,
///     mirroring <see cref="ReplaceBlockItemsHandlerConcurrencyTests" /> (the Block reference
///     implementation): the handler pre-check (stale If-Match → 412), the force-write path (no
///     If-Match), the unconditional Version bump on every save, and the EF concurrency-token backstop
///     that catches a writer that lost the load→save race. The backstop test is non-vacuous by
///     construction — remove the <c>IsConcurrencyToken()</c> config on Template and the losing save
///     silently succeeds instead of mapping to a <see cref="PreconditionFailedError" />.
/// </summary>
[TestFixture]
public class ReplaceTemplateItemsHandlerConcurrencyTests
{
    private InMemoryTvContext _db = null!;

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

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

    private async Task SeedTemplateAsync(int version)
    {
        await using TvContext ctx = _db.CreateContext();
        ctx.Blocks.Add(
            new Block
            {
                Id = 10,
                BlockGroupId = 1,
                Name = "Morning",
                Minutes = 30,
                StopScheduling = BlockStopScheduling.AfterDurationEnd,
                Items = new List<BlockItem>()
            });
        ctx.Templates.Add(
            new Template
            {
                Id = 1,
                TemplateGroupId = 1,
                Name = "Weekday",
                Version = version,
                Items = new List<TemplateItem>()
            });
        await ctx.SaveChangesAsync();
    }

    private static ReplaceTemplateItems Command(Option<int> expectedVersion) =>
        new(
            1,
            1,
            "Weekday",
            new List<ReplaceTemplateItem> { new(10, TimeSpan.Zero) },
            expectedVersion.Map(v => new[] { v }.ToSeq()));

    private async Task<int> ReadVersionAsync()
    {
        await using TvContext ctx = _db.CreateContext();
        return await ctx.Templates.Where(t => t.Id == 1).Select(t => t.Version).SingleAsync();
    }

    private static BaseError? LeftOrNull<T>(Either<BaseError, T> result) =>
        result.Match<BaseError?>(Right: _ => null, Left: e => e);

    [Test]
    public async Task Stale_If_Match_Should_Fail_Precondition_And_Not_Mutate()
    {
        await SeedTemplateAsync(version: 2);
        var handler = new ReplaceTemplateItemsHandler(_db.Factory);

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

        LeftOrNull(result).ShouldBeOfType<PreconditionFailedError>();

        // The pre-check runs before any mutation: version unchanged, no items written.
        (await ReadVersionAsync()).ShouldBe(2);
        await using TvContext ctx = _db.CreateContext();
        (await ctx.TemplateItems.CountAsync(i => i.TemplateId == 1)).ShouldBe(0);
    }

    [Test]
    public async Task Matching_If_Match_Should_Succeed_And_Bump_Version()
    {
        await SeedTemplateAsync(version: 2);
        var handler = new ReplaceTemplateItemsHandler(_db.Factory);

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

        result.IsRight.ShouldBeTrue();
        (await ReadVersionAsync()).ShouldBe(3);
    }

    [Test]
    public async Task Absent_If_Match_Should_Force_Write_And_Bump_Version()
    {
        await SeedTemplateAsync(version: 2);
        var handler = new ReplaceTemplateItemsHandler(_db.Factory);

        // None expected version = Phase-1 force-write regardless of the stored version.
        Either<BaseError, List<TemplateItemViewModel>> result =
            await handler.Handle(Command(None), CancellationToken.None);

        result.IsRight.ShouldBeTrue();
        (await ReadVersionAsync()).ShouldBe(3);
    }

    [Test]
    public async Task Save_Should_Bump_Version_Even_When_Content_Is_Unchanged()
    {
        await SeedTemplateAsync(version: 5);
        var handler = new ReplaceTemplateItemsHandler(_db.Factory);

        // Same content twice: the unconditional bump (M1) must still rotate the version each time,
        // otherwise a no-op PUT-back would not fire the token or rotate other clients' ETags.
        (await handler.Handle(Command(Some(5)), CancellationToken.None)).IsRight.ShouldBeTrue();
        (await ReadVersionAsync()).ShouldBe(6);
        (await handler.Handle(Command(Some(6)), CancellationToken.None)).IsRight.ShouldBeTrue();
        (await ReadVersionAsync()).ShouldBe(7);
    }

    [Test]
    public async Task Racing_Save_Should_Map_Concurrency_Failure_To_412()
    {
        await SeedTemplateAsync(version: 1);

        // Two writers load the same version, then both bump-and-save. The IsConcurrencyToken guard on
        // Template makes the second UPDATE key on the original version; it matches zero rows and
        // throws DbUpdateConcurrencyException, which the shared save helper maps to a
        // PreconditionFailedError.
        await using TvContext ctxWinner = _db.CreateContext();
        await using TvContext ctxLoser = _db.CreateContext();

        Template winner = await ctxWinner.Templates.SingleAsync(t => t.Id == 1);
        Template loser = await ctxLoser.Templates.SingleAsync(t => t.Id == 1);

        winner.Version++;
        Either<BaseError, Unit> winnerResult = await ctxWinner.SaveChangesWithConcurrencyGuard(CancellationToken.None);
        winnerResult.IsRight.ShouldBeTrue();

        loser.Version++;
        Either<BaseError, Unit> loserResult = await ctxLoser.SaveChangesWithConcurrencyGuard(CancellationToken.None);

        LeftOrNull(loserResult).ShouldBeOfType<PreconditionFailedError>();

        // Non-vacuous: the winner's write stuck at exactly its +1; the loser did not overwrite it.
        (await ReadVersionAsync()).ShouldBe(2);
    }
}
