using System.IO.Compression;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.Options;
using NSubstitute;
using TeleWave.Application.Broadcast.Bumpers;
using TeleWave.Application.Broadcast.Scheduling;
using TeleWave.Application.Common.Interfaces;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Planning;
using TeleWave.Application.Programming.Planning.DebugExport;
using TeleWave.Application.Programming.Templates;
using TeleWave.Application.Streaming;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Broadcast;
using TeleWave.Domain.Broadcast.Scheduling;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using TeleWave.Infrastructure.Persistence;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

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

    private sealed class FirstAlways : IRandomSource
    {
        public int Next(int maxExclusive) => 0;
    }

    private static MediaAsset ReadyAsset(string name, TimeSpan duration)
    {
        var asset = MediaAsset.Register(name, ".mkv", MediaSource.Upload);
        asset.MarkProcessing();
        asset.MarkReady(
            new MediaReadyInfo(
                duration,
                2,
                (int)(duration.TotalSeconds / 2),
                1920,
                1080,
                "h264",
                "aac",
                "assets/x"
            )
        );
        return asset;
    }

    private static async Task<(TestDb Fixture, Guid ChannelId)> SeedAsync()
    {
        var fixture = new TestDb();
        var channel = Channel.Create("Первый", "one", T0);

        var show = Show.Create("Симпсоны", ShowKind.Series);
        var episodes = new[]
        {
            ReadyAsset("s01e01.mkv", TimeSpan.FromMinutes(20)),
            ReadyAsset("s01e02.mkv", TimeSpan.FromMinutes(20)),
        };
        foreach (var episode in episodes)
            show.AddEpisode(episode.Id);

        var group = Group.Create("Мультсериалы");
        group.AddElement(GroupElementKind.Show, show.Id);

        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,
                new SlotStrategy(SlotStrategyType.Sequential).ToJson(),
                null,
                SlotBlockMode.Count,
                2,
                OverflowPolicy.ContinueNext
            )
        );
        channel.SetTemplate(template.Id);

        await using var seed = fixture.New();
        seed.MediaAssets.AddRange(episodes);
        seed.Shows.Add(show);
        seed.Groups.Add(group);
        seed.Channels.Add(channel);
        seed.ScheduleTemplates.Add(template);
        await seed.SaveChangesAsync(CancellationToken.None);

        return (fixture, channel.Id);
    }

    private static ExportChannelDebugCommandHandler Handler(
        AppDbContext db,
        IDebugExportStore store
    )
    {
        var random = new FirstAlways();
        var expander = new GroupExpander(db, GroupServices.Dynamic(db));
        var generator = new GridScheduleGenerator(
            db,
            expander,
            new BumperResolver(db, Substitute.For<IBumperRenderQueue>(), random),
            new PostCheckRunner(db),
            random,
            Options.Create(new SchedulerOptions { HorizonDays = 1, RetentionDays = 90 }),
            Options.Create(new StreamingOptions())
        );

        return new ExportChannelDebugCommandHandler(
            db,
            new ChannelDebugCollector(
                db,
                expander,
                generator,
                Options.Create(new SchedulerOptions { HorizonDays = 1, RetentionDays = 90 })
            ),
            store
        );
    }

    private static Dictionary<string, string> Unpack(byte[] archive)
    {
        using var buffer = new MemoryStream(archive);
        using var zip = new ZipArchive(buffer, ZipArchiveMode.Read);

        var files = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var entry in zip.Entries)
        {
            using var stream = entry.Open();
            using var reader = new StreamReader(stream, Encoding.UTF8);
            files[entry.FullName] = reader.ReadToEnd();
        }

        return files;
    }

    [Fact]
    public async Task Export_PacksEveryPartOfThePlannerInput()
    {
        var (fixture, channelId) = await SeedAsync();
        var store = Substitute.For<IDebugExportStore>();
        store
            .SaveAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>())
            .Returns("D:/debug-exports/dump.zip");

        await using var db = fixture.New();
        var result = await Handler(db, store)
            .Handle(new ExportChannelDebugCommand(channelId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        Assert.EndsWith(".zip", result.Value.FileName, StringComparison.Ordinal);
        Assert.Equal("D:/debug-exports/dump.zip", result.Value.SavedTo);

        var files = Unpack(result.Value.Content);
        Assert.Equal(
            [
                "README.md",
                "channel.json",
                "effective-grid.json",
                "groups.json",
                "junctions.json",
                "plan-preview.json",
                "schedule.json",
                "slot-states.json",
                "summary.json",
                "template.json",
            ],
            files.Keys.Order(StringComparer.Ordinal)
        );

        // Дамп обязан читаться глазами: рядом с идентификаторами стоят имена, а единицы группы
        // развёрнуты поштучно — иначе «почему вышла эта серия» по нему не разобрать.
        Assert.Contains("Симпсоны", files["groups.json"], StringComparison.Ordinal);
        Assert.Contains("Дневной блок", files["template.json"], StringComparison.Ordinal);
        Assert.Contains("Дневной блок", files["effective-grid.json"], StringComparison.Ordinal);

        using var groups = JsonDocument.Parse(files["groups.json"]);
        var units = groups
            .RootElement[0]
            .GetProperty("elements")[0]
            .GetProperty("units")
            .EnumerateArray()
            .ToList();
        Assert.Equal(2, units.Count);
        Assert.Equal("Симпсоны", units[0].GetProperty("show").GetString());
    }

    [Fact]
    public async Task Export_DryRunGoesIntoTheDump()
    {
        // Сухой прогон — половина ценности дампа: он показывает, что планировщик построил бы
        // сейчас, и без него по архиву не отличить «данные плохие» от «расчёт плохой».
        var (fixture, channelId) = await SeedAsync();
        // Каталог не настроен — хранилище так и отвечает: копии нет.
        var store = Substitute.For<IDebugExportStore>();
        store
            .SaveAsync(Arg.Any<string>(), Arg.Any<byte[]>(), Arg.Any<CancellationToken>())
            .Returns((string?)null);

        await using var db = fixture.New();
        var result = await Handler(db, store)
            .Handle(new ExportChannelDebugCommand(channelId), CancellationToken.None);

        Assert.True(result.IsSuccess);
        var files = Unpack(result.Value.Content);

        using var preview = JsonDocument.Parse(files["plan-preview.json"]);
        Assert.NotEqual(JsonValueKind.Null, preview.RootElement.ValueKind);
        Assert.NotEmpty(preview.RootElement.GetProperty("items").EnumerateArray());

        // Копии нет, но архив всё равно собран и уезжает в браузер.
        Assert.Null(result.Value.SavedTo);
    }

    [Fact]
    public async Task Export_FailsWithoutChannel()
    {
        var (fixture, _) = await SeedAsync();
        await using var db = fixture.New();

        var result = await Handler(db, Substitute.For<IDebugExportStore>())
            .Handle(new ExportChannelDebugCommand(Guid.NewGuid()), CancellationToken.None);

        Assert.False(result.IsSuccess);
    }
}
