using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.ExcludeGroupElement;
using TeleWave.Application.Programming.Groups.UpdateGroup;
using TeleWave.Application.Programming.Planning;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using TeleWave.Domain.Media;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Динамическая группа: состав считает правило, а список позиций хранит только ручные поправки.
/// Проверяем именно то, что уходит в эфир, — иначе расхождение между показанным и сыгранным.
/// </summary>
public class DynamicGroupsTests
{
    [Fact]
    public async Task Composition_IsRuleResult_PinnedFirstThenAlphabetical()
    {
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var beta = WithGenre(Show.Create("Бета", ShowKind.Series), genre);
        var alpha = WithGenre(Show.Create("Альфа", ShowKind.Series), genre);
        // Закреплённое правилу не подходит, но остаётся в составе — и идёт первым.
        var pinned = Show.Create("Закреплённое", ShowKind.Single);

        var group = Group.Create("Мультфильмы");
        group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
        group.AddElement(GroupElementKind.Show, pinned.Id);
        group.SetMode(GroupMode.Dynamic);

        await using (var seed = fixture.New())
        {
            seed.Genres.Add(genre);
            seed.Shows.AddRange(alpha, beta, pinned);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var composition = await ResolveAsync(fixture, group.Id);

        Assert.Equal([pinned.Id, alpha.Id, beta.Id], composition.Select(e => e.Id));
        Assert.True(composition[0].Pinned);
        Assert.All(composition.Skip(1), e => Assert.False(e.Pinned));
    }

    [Fact]
    public async Task Excluded_DropsOutOfComposition()
    {
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var keep = WithGenre(Show.Create("Остаётся", ShowKind.Series), genre);
        var drop = WithGenre(Show.Create("Выкинуть", ShowKind.Series), genre);

        var group = Group.Create("Мультфильмы");
        group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
        group.SetMode(GroupMode.Dynamic);

        await using (var seed = fixture.New())
        {
            seed.Genres.Add(genre);
            seed.Shows.AddRange(keep, drop);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using (var db = fixture.New())
        {
            var result = await new ExcludeGroupElementCommandHandler(
                db,
                GroupServices.Stats(db)
            ).Handle(
                new ExcludeGroupElementCommand(group.Id, GroupElementKind.Show, drop.Id, true),
                CancellationToken.None
            );
            Assert.True(result.IsSuccess);
            await db.SaveChangesAsync(CancellationToken.None);
        }

        var composition = await ResolveAsync(fixture, group.Id);
        Assert.Equal([keep.Id], composition.Select(e => e.Id));

        // Снятие исключения возвращает элемент правилу.
        await using (var db = fixture.New())
        {
            await new ExcludeGroupElementCommandHandler(db, GroupServices.Stats(db)).Handle(
                new ExcludeGroupElementCommand(group.Id, GroupElementKind.Show, drop.Id, false),
                CancellationToken.None
            );
            await db.SaveChangesAsync(CancellationToken.None);
        }

        Assert.Equal(2, (await ResolveAsync(fixture, group.Id)).Count);
    }

    [Fact]
    public async Task SwitchToDynamic_KeepsExistingItemsAsPinned()
    {
        // Смена режима не должна вымывать из эфира то, что уже стояло в слотах.
        var fixture = new TestDb();
        var show = Show.Create("A", ShowKind.Series);
        var group = Group.Create("Группа");
        group.AddElement(GroupElementKind.Show, show.Id);

        await using (var seed = fixture.New())
        {
            seed.Shows.Add(show);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await UpdateModeAsync(fixture, group.Id, GroupMode.Dynamic);

        await using var check = fixture.New();
        var stored = check.Groups.Include(g => g.Items).Single();
        Assert.Equal(GroupMode.Dynamic, stored.Mode);
        Assert.Equal(GroupItemRole.Pinned, stored.Items.Single().Role);
    }

    [Fact]
    public async Task SwitchToStatic_MaterializesComputedComposition()
    {
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var first = WithGenre(Show.Create("A", ShowKind.Series), genre);
        var second = WithGenre(Show.Create("B", ShowKind.Series), genre);

        var group = Group.Create("Мультфильмы");
        group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
        group.SetMode(GroupMode.Dynamic);

        await using (var seed = fixture.New())
        {
            seed.Genres.Add(genre);
            seed.Shows.AddRange(first, second);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await UpdateModeAsync(fixture, group.Id, GroupMode.Static);

        await using var check = fixture.New();
        var stored = check.Groups.Include(g => g.Items).Single();
        Assert.Equal(GroupMode.Static, stored.Mode);
        Assert.Equal(2, stored.Items.Count);
        Assert.All(stored.Items, item => Assert.Equal(GroupItemRole.Member, item.Role));
    }

    [Fact]
    public async Task Planner_ExpandsDynamicGroupByRule()
    {
        // Главное свойство: правило применяется на самой генерации, а не когда-то до неё.
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var asset = MediaAsset.Register("a.mkv", ".mkv", MediaSource.Upload);
        asset.MarkProcessing();
        asset.MarkReady(
            new MediaReadyInfo(
                TimeSpan.FromMinutes(22),
                2,
                660,
                1920,
                1080,
                "h264",
                "aac",
                "assets/a"
            )
        );
        var show = WithGenre(Show.Create("A", ShowKind.Series), genre);
        show.AddEpisode(asset.Id);

        var group = Group.Create("Мультфильмы");
        group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
        group.SetMode(GroupMode.Dynamic);

        await using (var seed = fixture.New())
        {
            seed.Genres.Add(genre);
            seed.MediaAssets.Add(asset);
            seed.Shows.Add(show);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var expanded = await new GroupExpander(db, GroupServices.Dynamic(db)).ExpandAsync(
            [group.Id],
            Guid.NewGuid(),
            CancellationToken.None
        );

        var element = Assert.Single(expanded[group.Id]);
        Assert.Equal(show.Id, element.ElementId);
        Assert.Single(element.Units);
    }

    private static Show WithGenre(Show show, Genre genre)
    {
        show.SetGenres([genre.Id]);
        return show;
    }

    private static async Task<IReadOnlyList<ResolvedElement>> ResolveAsync(
        TestDb fixture,
        Guid groupId
    )
    {
        await using var db = fixture.New();
        var group = await db.Groups.Include(g => g.Items).FirstAsync(g => g.Id == groupId);
        return await GroupServices.Dynamic(db).ResolveAsync(group, CancellationToken.None);
    }

    private static async Task UpdateModeAsync(TestDb fixture, Guid groupId, GroupMode mode)
    {
        await using var db = fixture.New();
        var group = await db.Groups.FirstAsync(g => g.Id == groupId);
        var result = await new UpdateGroupCommandHandler(
            db,
            GroupServices.Dynamic(db),
            GroupServices.Stats(db)
        ).Handle(
            new UpdateGroupCommand(
                groupId,
                group.Name,
                group.Description,
                GroupFilter.FromJson(group.FilterJson),
                mode
            ),
            CancellationToken.None
        );
        Assert.True(result.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);
    }
}
