using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Programming.Templates.Junctions;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Programming;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Команды стыка. Стык общий для всех каналов, поэтому проверки здесь про две вещи: источник врезки
/// должен существовать, а правка — помечать изменёнными все шаблоны, которые на этот стык ссылаются.
/// </summary>
public class JunctionHandlersTests
{
    private static readonly DateTimeOffset T0 = new(2026, 1, 1, 0, 0, 0, TimeSpan.Zero);

    private static JunctionElementInput Input(
        JunctionElementKind kind = JunctionElementKind.Ad,
        Guid? groupId = null,
        Guid? bumperTemplateId = null,
        Guid? bumperVariantId = null,
        int amountValue = 2,
        bool isRequired = false,
        string? choiceKey = null,
        int choiceWeight = 1,
        JunctionConditions? conditions = null
    ) =>
        new(
            kind,
            groupId,
            bumperTemplateId,
            bumperVariantId,
            JunctionAmountMode.Count,
            amountValue,
            isRequired,
            choiceKey,
            choiceWeight,
            conditions
        );

    /// <summary>Канал со своей сеткой и слотом, который ссылается на стык, — минимум для проверки отметок.</summary>
    private static async Task<(
        TestDb Fixture,
        Guid JunctionId,
        Guid TemplateId,
        Guid GroupId
    )> SeedAsync()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Реклама");
        var junction = JunctionTemplate.Create("Прайм");
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var slot = template.Background!.AddSlot("Круглосуточно", new TimeOnly(6, 0), 24 * 60);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                null,
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext,
                null,
                junction.Id
            )
        );
        channel.SetTemplate(template.Id);

        await using var seed = fixture.New();
        seed.Channels.Add(channel);
        seed.Groups.Add(group);
        seed.JunctionTemplates.Add(junction);
        seed.ScheduleTemplates.Add(template);
        await seed.SaveChangesAsync(CancellationToken.None);
        // Шаблон создаётся уже «изменённым» — снимаем отметку, иначе проверять нечего.
        template.MarkApplied("{}");
        await seed.SaveChangesAsync(CancellationToken.None);

        return (fixture, junction.Id, template.Id, group.Id);
    }

    private static Task<bool> HasPendingChangesAsync(AppDbContext db, Guid templateId) =>
        db
            .ScheduleTemplates.AsNoTracking()
            .Where(t => t.Id == templateId)
            .Select(t => t.Revision != t.AppliedRevision)
            .FirstAsync();

    [Fact]
    public async Task Create_AddsGlobalJunction()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();

        var result = await new CreateJunctionCommandHandler(db).Handle(
            new CreateJunctionCommand("  Ночь  "),
            CancellationToken.None
        );
        await db.SaveChangesAsync(CancellationToken.None);

        Assert.True(result.IsSuccess);
        await using var verify = fixture.New();
        var junction = await verify.JunctionTemplates.SingleAsync();
        Assert.Equal("Ночь", junction.Name);
    }

    [Fact]
    public async Task Update_RenamesAndMarksReferencingTemplates()
    {
        var (fixture, junctionId, templateId, _) = await SeedAsync();

        await using var db = fixture.New();
        var result = await new UpdateJunctionCommandHandler(db).Handle(
            new UpdateJunctionCommand(junctionId, "Ночь", 180),
            CancellationToken.None
        );
        await db.SaveChangesAsync(CancellationToken.None);

        Assert.True(result.IsSuccess);
        await using var verify = fixture.New();
        var junction = await verify.JunctionTemplates.AsNoTracking().SingleAsync();
        Assert.Equal("Ночь", junction.Name);
        Assert.Equal(180, junction.MaxTotalSeconds);
        // Стык общий: чужой канал не должен молча поехать по новым врезкам без применения.
        Assert.True(await HasPendingChangesAsync(verify, templateId));
    }

    [Fact]
    public async Task Update_UnknownJunction_ReturnsNotFound()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();

        var result = await new UpdateJunctionCommandHandler(db).Handle(
            new UpdateJunctionCommand(Guid.NewGuid(), "Ночь", null),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.JunctionNotFound, result.Error);
    }

    [Fact]
    public async Task Delete_RefusesWhenUsedBySlotOrDefault()
    {
        var (fixture, junctionId, templateId, _) = await SeedAsync();

        await using (var db = fixture.New())
        {
            var used = await new DeleteJunctionCommandHandler(db).Handle(
                new DeleteJunctionCommand(junctionId),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.JunctionInUse(string.Empty).Code, used.Error.Code);
        }

        // Снимаем ссылку слота, но оставляем стыком по умолчанию — удалять всё равно нельзя.
        await using (var db = fixture.New())
        {
            var template = await db
                .ScheduleTemplates.Include(t => t.Layers)
                    .ThenInclude(l => l.Slots)
                .SingleAsync(t => t.Id == templateId);
            var slot = template.Layers.SelectMany(l => l.Slots).Single();
            slot.UpdateContent(
                new SlotContent(
                    slot.Title,
                    slot.SlotKind,
                    slot.GroupId,
                    null,
                    null,
                    slot.BlockMode,
                    slot.BlockValue,
                    slot.OverflowPolicy
                )
            );
            template.SetDefaultJunction(junctionId);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.New())
        {
            var asDefault = await new DeleteJunctionCommandHandler(db).Handle(
                new DeleteJunctionCommand(junctionId),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.JunctionInUse(string.Empty).Code, asDefault.Error.Code);
        }
    }

    [Fact]
    public async Task Delete_RemovesUnusedJunction()
    {
        var fixture = new TestDb();
        var junction = JunctionTemplate.Create("Ночь");
        await using (var seed = fixture.New())
        {
            seed.JunctionTemplates.Add(junction);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new DeleteJunctionCommandHandler(db).Handle(
            new DeleteJunctionCommand(junction.Id),
            CancellationToken.None
        );
        await db.SaveChangesAsync(CancellationToken.None);

        Assert.True(result.IsSuccess);
        await using var verify = fixture.New();
        Assert.Empty(verify.JunctionTemplates);
    }

    [Fact]
    public async Task AddAndRemoveElement_TouchReferencingTemplates()
    {
        var (fixture, junctionId, templateId, _) = await SeedAsync();

        Guid elementId;
        await using (var db = fixture.New())
        {
            var added = await new AddJunctionElementCommandHandler(db).Handle(
                new AddJunctionElementCommand(junctionId, JunctionElementKind.Ad),
                CancellationToken.None
            );
            Assert.True(added.IsSuccess);
            elementId = added.Value;
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using (var verify = fixture.New())
            Assert.True(await HasPendingChangesAsync(verify, templateId));

        await using (var db = fixture.New())
        {
            var removed = await new RemoveJunctionElementCommandHandler(db).Handle(
                new RemoveJunctionElementCommand(junctionId, elementId),
                CancellationToken.None
            );
            Assert.True(removed.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.New())
        {
            var missing = await new RemoveJunctionElementCommandHandler(db).Handle(
                new RemoveJunctionElementCommand(junctionId, elementId),
                CancellationToken.None
            );
            Assert.Equal(TemplateErrors.JunctionElementNotFound, missing.Error);
        }
    }

    [Fact]
    public async Task UpdateElement_RequiresExistingSource()
    {
        var (fixture, junctionId, _, groupId) = await SeedAsync();
        var bumper = BumperTemplate.Create("Блок", "Текст 1");
        Guid elementId;

        await using (var db = fixture.New())
        {
            db.BumperTemplates.Add(bumper);
            var junction = await db
                .JunctionTemplates.Include(j => j.Elements)
                .SingleAsync(j => j.Id == junctionId);
            elementId = junction.AddElement(JunctionElementKind.Ad).Id;
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var context = fixture.New();
        var handler = new UpdateJunctionElementCommandHandler(context);

        var noGroup = await handler.Handle(
            new UpdateJunctionElementCommand(junctionId, elementId, Input()),
            CancellationToken.None
        );
        Assert.Equal(TemplateErrors.JunctionGroupRequired, noGroup.Error);

        var unknownGroup = await handler.Handle(
            new UpdateJunctionElementCommand(junctionId, elementId, Input(groupId: Guid.NewGuid())),
            CancellationToken.None
        );
        Assert.Equal(TemplateErrors.GroupNotFound, unknownGroup.Error);

        var noBumper = await handler.Handle(
            new UpdateJunctionElementCommand(
                junctionId,
                elementId,
                Input(kind: JunctionElementKind.Bumper)
            ),
            CancellationToken.None
        );
        Assert.Equal(BumperErrors.TemplateNotFound, noBumper.Error);

        var alienVariant = await handler.Handle(
            new UpdateJunctionElementCommand(
                junctionId,
                elementId,
                Input(
                    kind: JunctionElementKind.Bumper,
                    bumperTemplateId: bumper.Id,
                    bumperVariantId: Guid.NewGuid()
                )
            ),
            CancellationToken.None
        );
        Assert.Equal(BumperErrors.VariantNotFound, alienVariant.Error);

        var ok = await handler.Handle(
            new UpdateJunctionElementCommand(
                junctionId,
                elementId,
                Input(
                    groupId: groupId,
                    isRequired: true,
                    conditions: new JunctionConditions(true, 30, [Daypart.Prime], null, 40)
                )
            ),
            CancellationToken.None
        );
        Assert.True(ok.IsSuccess);
        await context.SaveChangesAsync(CancellationToken.None);

        await using var verify = fixture.New();
        var element = await verify.JunctionElements.AsNoTracking().SingleAsync();
        var conditions = JunctionConditions.FromJson(element.ConditionsJson);
        Assert.Equal(groupId, element.GroupId);
        Assert.True(element.IsRequired);
        Assert.Equal(40, conditions!.Chance);
        Assert.Equal([Daypart.Prime], conditions.Dayparts);
    }

    [Fact]
    public async Task UpdateElement_UnknownElement_ReturnsNotFound()
    {
        var (fixture, junctionId, _, groupId) = await SeedAsync();

        await using var db = fixture.New();
        var result = await new UpdateJunctionElementCommandHandler(db).Handle(
            new UpdateJunctionElementCommand(junctionId, Guid.NewGuid(), Input(groupId: groupId)),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.JunctionElementNotFound, result.Error);
    }

    [Fact]
    public async Task Reorder_AppliesOrderAndForkMembership()
    {
        var (fixture, junctionId, _, _) = await SeedAsync();
        Guid first;
        Guid second;

        await using (var db = fixture.New())
        {
            var junction = await db
                .JunctionTemplates.Include(j => j.Elements)
                .SingleAsync(j => j.Id == junctionId);
            first = junction.AddElement(JunctionElementKind.Ad).Id;
            second = junction.AddElement(JunctionElementKind.Promo).Id;
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.New())
        {
            // Перетаскивание меняет порядок и принадлежность к развилке одним жестом.
            var result = await new ReorderJunctionCommandHandler(db).Handle(
                new ReorderJunctionCommand(
                    junctionId,
                    [
                        new JunctionElementOrder(second, "fork"),
                        new JunctionElementOrder(first, "fork"),
                    ]
                ),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var verify = fixture.New();
        var elements = await verify
            .JunctionElements.AsNoTracking()
            .OrderBy(e => e.Position)
            .ToListAsync();
        Assert.Equal(second, elements[0].Id);
        Assert.All(elements, e => Assert.Equal("fork", e.ChoiceKey));
    }

    [Fact]
    public async Task Reorder_UnknownJunction_ReturnsNotFound()
    {
        var fixture = new TestDb();
        await using var db = fixture.New();

        var result = await new ReorderJunctionCommandHandler(db).Handle(
            new ReorderJunctionCommand(Guid.NewGuid(), []),
            CancellationToken.None
        );

        Assert.Equal(TemplateErrors.JunctionNotFound, result.Error);
    }

    [Fact]
    public async Task List_ResolvesNamesAndChannelUsage()
    {
        var (fixture, junctionId, templateId, groupId) = await SeedAsync();
        var bumper = BumperTemplate.Create("Блок", "Текст 1");

        await using (var db = fixture.New())
        {
            db.BumperTemplates.Add(bumper);
            var junction = await db
                .JunctionTemplates.Include(j => j.Elements)
                .SingleAsync(j => j.Id == junctionId);
            junction.Update("Прайм", 240);
            junction
                .AddElement(JunctionElementKind.Ad)
                .Update(
                    new JunctionElementSettings(
                        JunctionElementKind.Ad,
                        groupId,
                        null,
                        null,
                        JunctionAmountMode.Count,
                        2,
                        true,
                        null,
                        1,
                        null
                    )
                );
            junction
                .AddElement(JunctionElementKind.Bumper)
                .Update(
                    new JunctionElementSettings(
                        JunctionElementKind.Bumper,
                        null,
                        bumper.Id,
                        bumper.Variants[0].Id,
                        JunctionAmountMode.Count,
                        1,
                        false,
                        "fork",
                        3,
                        new JunctionConditions(OnlyOnElementChange: true).ToJson()
                    )
                );

            // Второй канал ссылается на тот же стык — счётчик обязан это увидеть.
            var other = Channel.Create("Второй", "two", T0);
            var otherTemplate = ScheduleTemplate.Create(other.Id, "Сетка 2");
            otherTemplate.SetDefaultJunction(junctionId);
            other.SetTemplate(otherTemplate.Id);
            db.Channels.Add(other);
            db.ScheduleTemplates.Add(otherTemplate);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        await using var read = fixture.New();
        var junctions = await new ListJunctionsQueryHandler(read).Handle(
            new ListJunctionsQuery(),
            CancellationToken.None
        );

        var dto = Assert.Single(junctions);
        Assert.Equal("Прайм", dto.Name);
        Assert.Equal(240, dto.MaxTotalSeconds);
        Assert.Equal(2, dto.ChannelUsageCount);
        Assert.Equal("Реклама", dto.Elements[0].GroupName);
        Assert.Equal("Блок", dto.Elements[1].BumperTemplateName);
        Assert.Equal("Текст 1", dto.Elements[1].BumperVariantName);
        Assert.Equal("fork", dto.Elements[1].ChoiceKey);
        Assert.True(dto.Elements[1].Conditions!.OnlyOnElementChange);
        Assert.Equal(templateId, templateId);
    }

    [Fact]
    public void Conditions_RoundTripAndDaypartFilter()
    {
        var conditions = new JunctionConditions(
            true,
            30,
            [Daypart.Prime, Daypart.Night],
            new JunctionTimeWindow(new TimeOnly(23, 0), new TimeOnly(6, 0)),
            40
        );

        var restored = JunctionConditions.FromJson(conditions.ToJson())!;

        // Сравниваем по полям: у записи со списком внутри равенство ссылочное.
        Assert.Equal(conditions.OnlyOnElementChange, restored.OnlyOnElementChange);
        Assert.Equal(conditions.MinMinutesBetween, restored.MinMinutesBetween);
        Assert.Equal(conditions.Dayparts, restored.Dayparts);
        Assert.Equal(conditions.TimeWindow, restored.TimeWindow);
        Assert.Equal(conditions.Chance, restored.Chance);
        Assert.True(restored.AllowsDaypart(Daypart.Prime));
        Assert.False(restored.AllowsDaypart(Daypart.Morning));
        // Пустой список дейпартов не ограничивает ничего — иначе врезка молча исчезла бы из эфира.
        Assert.True(new JunctionConditions().AllowsDaypart(Daypart.Morning));
        Assert.Null(JunctionConditions.FromJson("{не json}"));
        Assert.Null(JunctionConditions.FromJson(null));
    }
}
