using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.GetGroup;
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 GetGroupTests
{
    [Fact]
    public async Task StaticGroup_ReturnsItemsWithIdsAndStats()
    {
        var fixture = new TestDb();
        var asset = ReadyAsset(TimeSpan.FromMinutes(30));
        var show = Show.Create("Шоу", ShowKind.Series);
        show.AddEpisode(asset.Id);

        var group = Group.Create("Статическая");
        group.AddElement(GroupElementKind.Show, show.Id);

        await SeedAsync(fixture, group, [show], [asset]);

        var dto = await GetAsync(fixture, group.Id);

        var item = Assert.Single(dto.Items);
        // У позиции статической группы есть своя строка — её можно двигать и взвешивать.
        Assert.NotNull(item.Id);
        Assert.False(item.Pinned);
        Assert.Equal("Шоу", item.ElementName);
        Assert.Equal(1, dto.UnitCount);
        Assert.Equal(TimeSpan.FromMinutes(30).TotalSeconds, dto.TotalDurationSeconds);
        Assert.Empty(dto.Excluded);
    }

    [Fact]
    public async Task DynamicGroup_ShowsRuleMatchesWithoutItemId()
    {
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var byRule = Show.Create("По правилу", ShowKind.Series);
        byRule.SetGenres([genre.Id]);
        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(byRule, pinned);
            seed.Groups.Add(group);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var dto = await GetAsync(fixture, group.Id);

        Assert.Equal(GroupMode.Dynamic, dto.Mode);
        var first = dto.Items[0];
        Assert.True(first.Pinned);
        Assert.NotNull(first.Id);

        var second = dto.Items[1];
        Assert.False(second.Pinned);
        // Найденному правилом строки в таблице не отвечает — значит, ни веса, ни порядка.
        Assert.Null(second.Id);
    }

    [Fact]
    public async Task DynamicGroup_ListsExclusionsSeparately()
    {
        // Иначе непонятно, почему шоу нет в составе, хотя правило его находит.
        var fixture = new TestDb();
        var genre = Genre.Create("Мультфильм", "animation");
        var dropped = Show.Create("Исключённое", ShowKind.Series);
        dropped.SetGenres([genre.Id]);

        var group = Group.Create("Динамическая");
        group.SetFilter(new GroupFilter(GenreIds: [genre.Id]).ToJson());
        group.SetMode(GroupMode.Dynamic);
        group.Exclude(GroupElementKind.Show, dropped.Id);

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

        var dto = await GetAsync(fixture, group.Id);

        Assert.Empty(dto.Items);
        var excluded = Assert.Single(dto.Excluded);
        Assert.Equal("Исключённое", excluded.ElementName);
    }

    [Fact]
    public async Task MissingElement_ShowsDashInsteadOfFailing()
    {
        // Элемент мог исчезнуть из библиотеки между чисткой и чтением — экран группы падать не должен.
        var fixture = new TestDb();
        var group = Group.Create("Со следом");
        group.AddElement(GroupElementKind.Show, Guid.NewGuid());
        await SeedAsync(fixture, group, [], []);

        var dto = await GetAsync(fixture, group.Id);

        Assert.Equal("—", Assert.Single(dto.Items).ElementName);
    }

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

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

        Assert.False(result.IsSuccess);
    }

    private static MediaAsset ReadyAsset(TimeSpan duration)
    {
        var asset = MediaAsset.Register($"{Guid.NewGuid():N}.mkv", ".mkv", MediaSource.Upload);
        asset.MarkProcessing();
        asset.MarkReady(
            new MediaReadyInfo(duration, 2, 900, 1920, 1080, "h264", "aac", "assets/x")
        );
        return asset;
    }

    private static async Task SeedAsync(
        TestDb fixture,
        Group group,
        Show[] shows,
        MediaAsset[] assets
    )
    {
        await using var seed = fixture.New();
        seed.MediaAssets.AddRange(assets);
        seed.Shows.AddRange(shows);
        seed.Groups.Add(group);
        await seed.SaveChangesAsync(CancellationToken.None);
    }

    private static GetGroupQueryHandler Handler(Infrastructure.Persistence.AppDbContext db) =>
        new(db, new GroupElementResolver(db), GroupServices.Dynamic(db));

    private static async Task<GroupDto> GetAsync(TestDb fixture, Guid groupId)
    {
        await using var db = fixture.New();
        var result = await Handler(db).Handle(new GetGroupQuery(groupId), CancellationToken.None);
        Assert.True(result.IsSuccess);
        return result.Value;
    }
}
