using Microsoft.Extensions.Options;
using TeleWave.Application.Tests.TestSupport;
using TeleWave.Domain.Library;
using TeleWave.Infrastructure.Metadata;
using Xunit;

namespace TeleWave.Application.Tests.Metadata;

/// <summary>
/// Разбор ответов TMDb. Наружу тесты не ходят — HTTP подменён, проверяем и собранный адрес
/// (сериалы и фильмы живут в разных пространствах API), и то, что из ответа вычитано.
/// </summary>
public class TmdbMetadataProviderTests
{
    private static TmdbMetadataProvider Provider(FakeHttpClientFactory http) =>
        new(
            http,
            Options.Create(
                new MetadataOptions
                {
                    Language = "ru-RU",
                    Tmdb = new TmdbOptions { ApiKey = "key" },
                }
            )
        );

    [Fact]
    public async Task SearchShows_Series_ReadsNameAndFirstAirDate()
    {
        var http = FakeHttpClientFactory.Json(
            """
            {"results":[
              {"id":42,"name":"Сериал","first_air_date":"2001-05-06","overview":"о чём-то","poster_path":"/p.jpg"},
              {"name":"без идентификатора"}
            ]}
            """
        );

        var found = await Provider(http)
            .SearchShowsAsync("сериал", ShowKind.Series, CancellationToken.None);

        Assert.Contains("/search/tv", http.LastUrl);
        Assert.Contains("language=ru-RU", http.LastUrl);
        // Запись без id пропущена: обратиться по ней всё равно не получится.
        var candidate = Assert.Single(found);
        Assert.Equal("42", candidate.ExternalId);
        Assert.Equal("Сериал", candidate.Title);
        Assert.Equal(2001, candidate.Year);
        Assert.Equal("о чём-то", candidate.Overview);
        Assert.Equal("https://image.tmdb.org/t/p/w500/p.jpg", candidate.PosterUrl);
        Assert.Equal(ShowKind.Series, candidate.Kind);
    }

    [Fact]
    public async Task SearchShows_Single_UsesMovieNamespace()
    {
        var http = FakeHttpClientFactory.Json(
            """{"results":[{"id":7,"title":"Фильм","release_date":"1999-03-31"}]}"""
        );

        var found = await Provider(http)
            .SearchShowsAsync("фильм", ShowKind.Single, CancellationToken.None);

        Assert.Contains("/search/movie", http.LastUrl);
        var candidate = Assert.Single(found);
        Assert.Equal("Фильм", candidate.Title);
        Assert.Equal(1999, candidate.Year);
        Assert.Null(candidate.PosterUrl);
        Assert.Equal(ShowKind.Single, candidate.Kind);
    }

    [Fact]
    public async Task SearchShows_ReturnsEmpty_WhenNoResultsSection()
    {
        var found = await Provider(FakeHttpClientFactory.Json("{}"))
            .SearchShowsAsync("ничего", ShowKind.Series, CancellationToken.None);

        Assert.Empty(found);
    }

    [Fact]
    public async Task GetShow_Series_ReadsGenresAndContentRating()
    {
        var http = FakeHttpClientFactory.Json(
            """
            {"name":"Сериал","first_air_date":"2001-05-06","overview":"описание","poster_path":"/p.jpg",
             "genres":[{"id":28,"name":"Боевик"},{"name":"Драма"},{"id":99}],
             "content_ratings":{"results":[{"iso_3166_1":"RU","rating":"16+"},{"iso_3166_1":"US","rating":"TV-14"}]}}
            """
        );

        var show = await Provider(http).GetShowAsync("42", ShowKind.Series, CancellationToken.None);

        Assert.Contains("/tv/42", http.LastUrl);
        Assert.Contains("append_to_response=content_ratings", http.LastUrl);
        Assert.NotNull(show);
        Assert.Equal("Сериал", show.Title);
        Assert.Equal(2001, show.Year);
        // Идентификатор впереди названия, безымянный жанр — только идентификатором.
        Assert.Equal(["tmdb:28", "Боевик", "Драма", "tmdb:99"], show.Genres);
        // US приоритетнее RU, хотя в ответе идёт вторым.
        Assert.Equal("TV-14", show.ContentRating);
    }

    [Fact]
    public async Task GetShow_Movie_PrefersTheatricalCertification()
    {
        // Постер в ответе есть — значит, добора язык-нейтрального не будет и запрос ровно один.
        var http = FakeHttpClientFactory.Json(
            """
            {"title":"Фильм","poster_path":"/p.jpg","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[
              {"type":4,"certification":"R"},
              {"type":3,"certification":"PG-13"}
            ]}]}}
            """
        );

        var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);

        Assert.Contains("/movie/7", http.LastUrl);
        Assert.Contains("append_to_response=release_dates", http.LastUrl);
        Assert.Equal("PG-13", show?.ContentRating);
    }

    /// <summary>
    /// TMDb отдаёт постер под запрошенный язык, и у шоу без локализованной обложки поле пустое —
    /// хотя в поиске картинка была. Без добора «Применить» молча оставляло бы шоу без постера.
    /// </summary>
    [Fact]
    public async Task GetShow_WithoutLocalizedPoster_FallsBackToNeutralOne()
    {
        // Локализованный ответ (с language) — без постера, язык-нейтральный — с ним.
        var http = new FakeHttpClientFactory(url =>
            FakeHttpClientFactory.Ok(
                url.Contains("language=", StringComparison.Ordinal)
                    ? """{"name":"Сериал"}"""
                    : """{"name":"Сериал","poster_path":"/neutral.jpg"}"""
            )
        );

        var show = await Provider(http).GetShowAsync("9", ShowKind.Series, CancellationToken.None);

        Assert.Equal(2, http.Urls.Count);
        Assert.DoesNotContain("language=", http.LastUrl);
        Assert.Contains("/tv/9", http.LastUrl);
        Assert.EndsWith("/neutral.jpg", show?.PosterUrl);
    }

    /// <summary>Кадр серии — та же история, что и с постером шоу: под русский язык его часто нет.</summary>
    [Fact]
    public async Task GetEpisode_WithoutLocalizedStill_FallsBackToNeutralOne()
    {
        var http = new FakeHttpClientFactory(url =>
            FakeHttpClientFactory.Ok(
                url.Contains("language=", StringComparison.Ordinal)
                    ? """{"name":"Серия"}"""
                    : """{"name":"Серия","still_path":"/neutral.jpg"}"""
            )
        );

        var episode = await Provider(http).GetEpisodeAsync("9", 2, 5, CancellationToken.None);

        Assert.Equal(2, http.Urls.Count);
        Assert.DoesNotContain("language=", http.LastUrl);
        Assert.Contains("/tv/9/season/2/episode/5", http.LastUrl);
        Assert.EndsWith("/neutral.jpg", episode?.StillUrl);
    }

    [Fact]
    public async Task GetShow_Movie_FallsBackToAnyReleaseCertification()
    {
        var http = FakeHttpClientFactory.Json(
            """
            {"title":"Фильм","release_dates":{"results":[{"iso_3166_1":"US","release_dates":[
              {"type":5,"certification":"NC-17"}
            ]}]}}
            """
        );

        var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);

        Assert.Equal("NC-17", show?.ContentRating);
    }

    [Fact]
    public async Task GetShow_Movie_SkipsCountriesWithoutCertification()
    {
        var http = FakeHttpClientFactory.Json(
            """
            {"title":"Фильм","release_dates":{"results":[
              {"iso_3166_1":"US"},
              {"iso_3166_1":"US","release_dates":[{"type":3,"certification":"  "}]},
              {"iso_3166_1":"RU","release_dates":[{"type":3,"certification":"18+"}]}
            ]}}
            """
        );

        var show = await Provider(http).GetShowAsync("7", ShowKind.Single, CancellationToken.None);

        // У США записи есть, но сертификации в них нет — ответом становится следующая страна.
        Assert.Equal("18+", show?.ContentRating);
    }

    [Fact]
    public async Task GetShow_WithoutOptionalFields_FallsBackToPlaceholders()
    {
        var show = await Provider(FakeHttpClientFactory.Json("{}"))
            .GetShowAsync("42", ShowKind.Series, CancellationToken.None);

        Assert.NotNull(show);
        Assert.Equal("42", show.ExternalId);
        Assert.Equal("—", show.Title);
        Assert.Null(show.Year);
        Assert.Null(show.PosterUrl);
        Assert.Null(show.ContentRating);
        Assert.Empty(show.Genres!);
    }

    [Fact]
    public async Task GetShow_ReturnsNull_WhenRequestFails()
    {
        var show = await Provider(FakeHttpClientFactory.Failing())
            .GetShowAsync("42", ShowKind.Series, CancellationToken.None);

        Assert.Null(show);
    }

    [Fact]
    public async Task GetEpisode_ReadsStillAndAirDate()
    {
        var http = FakeHttpClientFactory.Json(
            """{"name":"Серия","overview":"о серии","still_path":"/s.jpg","air_date":"2001-06-07"}"""
        );

        var episode = await Provider(http).GetEpisodeAsync("42", 2, 3, CancellationToken.None);

        Assert.Contains("/tv/42/season/2/episode/3", http.LastUrl);
        Assert.NotNull(episode);
        Assert.Equal("Серия", episode.Title);
        Assert.Equal("о серии", episode.Overview);
        Assert.Equal("https://image.tmdb.org/t/p/w300/s.jpg", episode.StillUrl);
        Assert.Equal(new DateOnly(2001, 6, 7), episode.AirDate);
    }

    [Fact]
    public async Task GetEpisode_ReturnsNull_WhenRequestFails()
    {
        var episode = await Provider(FakeHttpClientFactory.Failing())
            .GetEpisodeAsync("42", 1, 1, CancellationToken.None);

        Assert.Null(episode);
    }

    [Fact]
    public async Task GetSeasonEpisodeCount_CountsEpisodes()
    {
        var http = FakeHttpClientFactory.Json("""{"episodes":[{"id":1},{"id":2},{"id":3}]}""");

        var count = await Provider(http)
            .GetSeasonEpisodeCountAsync("42", 2, CancellationToken.None);

        Assert.Contains("/tv/42/season/2", http.LastUrl);
        Assert.Equal(3, count);
    }

    [Fact]
    public async Task GetSeasonEpisodeCount_ReturnsNull_WhenSectionMissing()
    {
        var count = await Provider(FakeHttpClientFactory.Json("{}"))
            .GetSeasonEpisodeCountAsync("42", 9, CancellationToken.None);

        Assert.Null(count);
    }

    [Fact]
    public async Task GetSeasonNumbers_SkipsSpecialsAndSorts()
    {
        var http = FakeHttpClientFactory.Json(
            """{"seasons":[{"season_number":2},{"season_number":0},{"season_number":1},{"name":"без номера"}]}"""
        );

        var seasons = await Provider(http).GetSeasonNumbersAsync("42", CancellationToken.None);

        Assert.Equal([1, 2], seasons);
    }

    [Fact]
    public async Task GetSeasonNumbers_ReturnsEmpty_WhenRequestFails()
    {
        var seasons = await Provider(FakeHttpClientFactory.Failing())
            .GetSeasonNumbersAsync("42", CancellationToken.None);

        Assert.Empty(seasons);
    }
}
