using System.Text.Json;
using System.Text.Json.Serialization;
using TeleWave.Application.Broadcast;
using TeleWave.Application.Programming.Planning.Trace;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using TeleWave.Domain.Programming.Planning;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

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

    /// <summary>Те же настройки, что при записи трейса генератором.</summary>
    private static readonly JsonSerializerOptions TraceJson = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        Converters = { new JsonStringEnumConverter() },
    };

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

        var result = await new GetEntryTraceQueryHandler(db).Handle(
            new GetEntryTraceQuery(Guid.NewGuid()),
            CancellationToken.None
        );

        Assert.Equal(ChannelErrors.NotFound, result.Error);
    }

    [Fact]
    public async Task Trace_ResolvesSlotLayerGroupAndJunction()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var group = Group.Create("Кино");
        var collection = Collection.Create("Франшиза");
        var show = Show.Create("Фильм", ShowKind.Single);
        var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload);

        var junction = JunctionTemplate.Create("Прайм");
        var template = ScheduleTemplate.Create(channel.Id, "Сетка");
        var layer = template.AddLayer("Прайм", 10);
        var slot = layer.AddSlot("Вечернее кино", new TimeOnly(20, 0), 120);
        slot.UpdateContent(
            new SlotContent(
                slot.Title,
                SlotKind.Content,
                group.Id,
                new SlotStrategy(SlotStrategyType.RandomWithCooldown, CooldownDays: 14).ToJson(),
                null,
                SlotBlockMode.FillSlot,
                1,
                OverflowPolicy.ContinueNext,
                JunctionAfterId: junction.Id
            )
        );

        var trace = new PlanTrace(
            slot.Id,
            SlotKind.Content,
            GroupElementKind.Show,
            show.Id,
            SlotStrategyKind.RandomWithCooldown,
            CandidatesAfterCooldown: 4,
            DriftMinutes: 3,
            Snapped: true
        );
        var entry = ScheduleEntry.FromSlot(
            channel.Id,
            asset.Id,
            ScheduleEntryKind.Program,
            T0,
            T0.AddMinutes(90),
            new ScheduleEntryOrigin(
                show.Id,
                2,
                slot.Id,
                JsonSerializer.Serialize(trace, TraceJson),
                collection.Id
            )
        );

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Groups.Add(group);
            seed.Collections.Add(collection);
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.JunctionTemplates.Add(junction);
            seed.ScheduleTemplates.Add(template);
            seed.ScheduleEntries.Add(entry);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new GetEntryTraceQueryHandler(db).Handle(
            new GetEntryTraceQuery(entry.Id),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        var dto = result.Value;
        Assert.Equal("Фильм", dto.ShowName);
        Assert.Equal(2, dto.EpisodeIndex);
        Assert.Equal("Франшиза", dto.CollectionName);
        Assert.Equal("Прайм", dto.LayerName);
        Assert.Equal(10, dto.LayerPriority);
        Assert.Equal("Вечернее кино", dto.SlotTitle);
        Assert.Equal(new TimeOnly(20, 0), dto.SlotTargetStart);
        Assert.Equal(120, dto.SlotDurationMinutes);
        Assert.Equal("Кино", dto.GroupName);
        Assert.Equal(SlotStrategyKind.RandomWithCooldown, dto.Strategy);
        // Остывание берётся из текущей стратегии слота, а число кандидатов — из трейса.
        Assert.Equal(14, dto.CooldownDays);
        Assert.Equal(4, dto.CandidatesAfterCooldown);
        Assert.Equal(3, dto.DriftMinutes);
        Assert.True(dto.Snapped);
        Assert.Equal("Прайм", dto.JunctionName);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("{не json}")]
    public async Task Trace_WithoutUsableTrace_ReturnsBareEntry(string? traceJson)
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var show = Show.Create("Фильм", ShowKind.Single);
        var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload);
        var entry = ScheduleEntry.FromSlot(
            channel.Id,
            asset.Id,
            ScheduleEntryKind.Program,
            T0,
            T0.AddMinutes(90),
            new ScheduleEntryOrigin(show.Id, 1, null, traceJson)
        );

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.Shows.Add(show);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(entry);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new GetEntryTraceQueryHandler(db).Handle(
            new GetEntryTraceQuery(entry.Id),
            CancellationToken.None
        );

        // Старая запись (или битый трейс) — ответ есть, подписи пустые.
        Assert.True(result.IsSuccess);
        Assert.Equal("Фильм", result.Value.ShowName);
        Assert.Null(result.Value.LayerName);
        Assert.Null(result.Value.SlotTitle);
        Assert.Null(result.Value.Strategy);
        Assert.Equal(0, result.Value.DriftMinutes);
        Assert.False(result.Value.Snapped);
    }

    [Fact]
    public async Task Trace_WithDeletedSlot_KeepsWhatTraceRemembers()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);
        var asset = MediaAsset.Register("film.mkv", ".mkv", MediaSource.Upload);
        var trace = new PlanTrace(
            Guid.NewGuid(),
            SlotKind.Repeat,
            null,
            null,
            SlotStrategyKind.Sequential,
            null,
            DriftMinutes: 0,
            Snapped: false
        );
        var entry = ScheduleEntry.FromSlot(
            channel.Id,
            asset.Id,
            ScheduleEntryKind.Program,
            T0,
            T0.AddMinutes(30),
            new ScheduleEntryOrigin(null, null, null, JsonSerializer.Serialize(trace, TraceJson))
        );

        await using (var seed = fixture.New())
        {
            seed.Channels.Add(channel);
            seed.MediaAssets.Add(asset);
            seed.ScheduleEntries.Add(entry);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new GetEntryTraceQueryHandler(db).Handle(
            new GetEntryTraceQuery(entry.Id),
            CancellationToken.None
        );

        Assert.True(result.IsSuccess);
        // Слота уже нет — из трейса остаётся его тип и стратегия, остальное пустеет.
        Assert.Equal(SlotKind.Repeat, result.Value.SlotKind);
        Assert.Equal(SlotStrategyKind.Sequential, result.Value.Strategy);
        Assert.Null(result.Value.SlotTitle);
        Assert.Null(result.Value.GroupName);
        Assert.Null(result.Value.JunctionName);
        Assert.Null(result.Value.ShowName);
    }
}
