using Microsoft.EntityFrameworkCore;
using TeleWave.Application.Programming.Groups;
using TeleWave.Application.Programming.Groups.Suggest;
using TeleWave.Application.Tests.Support;
using TeleWave.Domain.Library;
using TeleWave.Domain.Programming;
using Xunit;

namespace TeleWave.Application.Tests.Programming;

/// <summary>
/// Предложения групп: разбор библиотеки должен быть объяснимым — что показали, то и создастся.
/// </summary>
public class GroupSuggestionsTests
{
    [Fact]
    public async Task SuggestsGenre_WhenAtLeastTwoShowsShareIt()
    {
        var fixture = new TestDb();
        var cartoons = Genre.Create("Мультфильм", "animation");
        var drama = Genre.Create("Драма", "drama");
        var first = WithGenre(Show.Create("A", ShowKind.Series), cartoons);
        var second = WithGenre(Show.Create("B", ShowKind.Series), cartoons);
        // Один драматический — на группу не тянет.
        var lonely = WithGenre(Show.Create("C", ShowKind.Single), drama);

        await using (var seed = fixture.New())
        {
            seed.Genres.AddRange(cartoons, drama);
            seed.Shows.AddRange(first, second, lonely);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var suggestions = await BuildAsync(fixture);

        var genre = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.Genre);
        Assert.Equal("Мультфильм", genre.Name);
        Assert.Equal(2, genre.ShowIds.Count);
        Assert.Equal([cartoons.Id], genre.Filter!.GenreIds);
    }

    [Fact]
    public async Task SuggestsBigSeries_Separately()
    {
        var fixture = new TestDb();
        var big = Show.Create("Гравити Фолз", ShowKind.Series);
        for (var i = 0; i < 20; i++)
            big.AddEpisode(Guid.NewGuid());
        var small = Show.Create("Короткое", ShowKind.Series);
        small.AddEpisode(Guid.NewGuid());

        await using (var seed = fixture.New())
        {
            seed.Shows.AddRange(big, small);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var suggestions = await BuildAsync(fixture);

        var series = Assert.Single(suggestions, s => s.Kind == GroupSuggestionKind.BigSeries);
        Assert.Equal("Гравити Фолз", series.Name);
        Assert.Equal([big.Id], series.ShowIds);
        // Правила у одиночного шоу нет: «именно это шоу» фильтром не выражается.
        Assert.Null(series.Filter);
    }

    [Fact]
    public async Task SkipsAudienceTier_WhenItCoversEverything()
    {
        // Если под «не строже PG» попадает вся библиотека, группа повторяет её — предлагать нечего.
        var fixture = new TestDb();
        var first = Show.Create("A", ShowKind.Series);
        first.SetAudience(ShowAudience.G);
        var second = Show.Create("B", ShowKind.Series);
        second.SetAudience(ShowAudience.Pg);

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

        var suggestions = await BuildAsync(fixture);

        Assert.DoesNotContain(suggestions, s => s.Kind == GroupSuggestionKind.Audience);
    }

    [Fact]
    public async Task SuggestsAudienceTier_WhenStricterContentExists()
    {
        var fixture = new TestDb();
        var kids = Show.Create("A", ShowKind.Series);
        kids.SetAudience(ShowAudience.G);
        var adult = Show.Create("B", ShowKind.Series);
        adult.SetAudience(ShowAudience.R);
        var unrated = Show.Create("C", ShowKind.Series);

        await using (var seed = fixture.New())
        {
            seed.Shows.AddRange(kids, adult, unrated);
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        var suggestions = await BuildAsync(fixture);

        var tier = suggestions.First(s => s.Kind == GroupSuggestionKind.Audience);
        // Непроставленный рейтинг остаётся в выборке — как в правиле набора и в планировщике.
        Assert.Equal(2, tier.ShowIds.Count);
        Assert.Contains(kids.Id, tier.ShowIds);
        Assert.Contains(unrated.Id, tier.ShowIds);
    }

    [Fact]
    public async Task CreateFromSuggestion_SavesFilterAndComposition()
    {
        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);

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

        await using var db = fixture.New();
        var handler = new CreateGroupFromSuggestionCommandHandler(
            db,
            Builder(db),
            GroupServices.Stats(db)
        );

        var created = await handler.Handle(
            new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
            CancellationToken.None
        );
        Assert.True(created.IsSuccess);
        await db.SaveChangesAsync(CancellationToken.None);

        await using var check = fixture.New();
        var group = check.Groups.Include(g => g.Items).Single();
        Assert.Equal("Мультфильм", group.Name);
        Assert.Equal(2, group.Items.Count);
        Assert.All(group.Items, item => Assert.Equal(GroupElementKind.Show, item.ElementKind));
        Assert.Equal([genre.Id], GroupFilter.FromJson(group.FilterJson)!.GenreIds);

        // Повторное создание того же — конфликт: две одноимённые группы в слоте неразличимы.
        var again = await handler.Handle(
            new CreateGroupFromSuggestionCommand($"genre:{genre.Id:N}"),
            CancellationToken.None
        );
        Assert.False(again.IsSuccess);
    }

    [Fact]
    public async Task CreateFromSuggestion_FailsForUnknownKey()
    {
        var fixture = new TestDb();
        await using (var seed = fixture.New())
        {
            seed.Shows.Add(Show.Create("A", ShowKind.Series));
            await seed.SaveChangesAsync(CancellationToken.None);
        }

        await using var db = fixture.New();
        var result = await new CreateGroupFromSuggestionCommandHandler(
            db,
            Builder(db),
            GroupServices.Stats(db)
        ).Handle(new CreateGroupFromSuggestionCommand("genre:нет-такого"), CancellationToken.None);

        Assert.False(result.IsSuccess);
    }

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

    private static GroupSuggestionBuilder Builder(Infrastructure.Persistence.AppDbContext db) =>
        new(db, new GroupElementResolver(db));

    private static async Task<IReadOnlyList<GroupSuggestion>> BuildAsync(TestDb fixture)
    {
        await using var db = fixture.New();
        return await Builder(db).BuildAsync(CancellationToken.None);
    }
}
