using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Core.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Search;
using ErsatzTV.Tests.Support;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Application.Channels;

[TestFixture]
public class GetAutoTuneChannelMembersHandlerTests
{
    private InMemoryTvContext _db = null!;
    private ISearchIndex _searchIndex = null!;

    [SetUp]
    public async Task SetUp()
    {
        _db = await InMemoryTvContext.CreateAsync();
        _searchIndex = Substitute.For<ISearchIndex>();
    }

    [TearDown]
    public async Task TearDown() => await _db.DisposeAsync();

    private void ReturnsSearch(params SearchItem[] items) =>
        _searchIndex.Search(
                Arg.Any<string>(),
                Arg.Any<string>(),
                Arg.Any<int>(),
                Arg.Any<int>(),
                Arg.Any<CancellationToken>())
            .Returns(new SearchResult(items.ToList(), items.Length));

    [Test]
    public async Task Handle_Should_Generate_The_Server_Owned_Query_For_The_Axis_Value()
    {
        ReturnsSearch();
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "Comedy", 0, 100),
            CancellationToken.None);

        // The client never sends Lucene; the handler builds it from AutoTuneAxisMap.GenerateQuery.
        await _searchIndex.Received(1).Search(
            Arg.Is<string>(q => q == "type:episode AND genre:\"Comedy\""),
            string.Empty,
            0,
            10_000,
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Handle_Should_Not_Search_For_A_Blank_Value()
    {
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel result = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "  ", 0, 100),
            CancellationToken.None);

        result.TotalCount.ShouldBe(0);
        result.Page.ShouldBeEmpty();
        await _searchIndex.DidNotReceive().Search(
            Arg.Any<string>(),
            Arg.Any<string>(),
            Arg.Any<int>(),
            Arg.Any<int>(),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Handle_Should_Roll_Episodes_Up_To_Distinct_Parent_Shows_With_Matching_Counts()
    {
        await SeedTwoShowGenreGraph();
        // Show 10 contributes episodes 101 & 102; show 20 contributes episode 201. Episode 103 (show 10)
        // exists but does NOT match the query, so it must not inflate show 10's count.
        ReturnsSearch(
            new SearchItem(LuceneSearchIndex.EpisodeType, 101),
            new SearchItem(LuceneSearchIndex.EpisodeType, 102),
            new SearchItem(LuceneSearchIndex.EpisodeType, 201));
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel result = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "Comedy", 0, 100),
            CancellationToken.None);

        result.TotalCount.ShouldBe(2);
        // Ordered by title: "Alpha Show" (10) before "Beta Show" (20).
        result.Page.Count.ShouldBe(2);
        result.Page[0].MediaType.ShouldBe(LibraryBrowseMediaType.TelevisionShow);
        result.Page[0].Title.ShouldBe("Alpha Show");
        result.Page[0].Id.ShouldBe(10);
        result.Page[0].ItemCount.ShouldBe(2); // matching episodes only, not the show's 3 total
        result.Page[1].Title.ShouldBe("Beta Show");
        result.Page[1].Id.ShouldBe(20);
        result.Page[1].ItemCount.ShouldBe(1);
    }

    [Test]
    public async Task Handle_Should_Page_Distinct_Shows_By_Title()
    {
        await SeedTwoShowGenreGraph();
        ReturnsSearch(
            new SearchItem(LuceneSearchIndex.EpisodeType, 101),
            new SearchItem(LuceneSearchIndex.EpisodeType, 201));
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel page0 = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "Comedy", 0, 1),
            CancellationToken.None);
        PagedLibraryBrowseItemsResponseModel page1 = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "Comedy", 1, 1),
            CancellationToken.None);

        page0.TotalCount.ShouldBe(2);
        page0.Page.Count.ShouldBe(1);
        page0.Page[0].Title.ShouldBe("Alpha Show");
        page1.TotalCount.ShouldBe(2);
        page1.Page.Count.ShouldBe(1);
        page1.Page[0].Title.ShouldBe("Beta Show");
    }

    [Test]
    public async Task Handle_Should_Return_Movies_As_Members_For_The_Movie_Genre_Axis()
    {
        await SeedMovieGenreGraph();
        ReturnsSearch(
            new SearchItem(LuceneSearchIndex.MovieType, 30),
            new SearchItem(LuceneSearchIndex.MovieType, 31));
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel result = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.MovieGenre, "Comedy", 0, 100),
            CancellationToken.None);

        result.TotalCount.ShouldBe(2);
        // Ordered by title: "Aardvark" before "Zebra".
        result.Page[0].MediaType.ShouldBe(LibraryBrowseMediaType.Movie);
        result.Page[0].Title.ShouldBe("Aardvark");
        result.Page[0].Id.ShouldBe(31);
        result.Page[0].ItemCount.ShouldBe(1);
        result.Page[1].Title.ShouldBe("Zebra");
        result.Page[1].Id.ShouldBe(30);
    }

    [Test]
    public async Task Handle_Should_Return_Empty_When_The_Query_Matches_Nothing()
    {
        await SeedTwoShowGenreGraph();
        ReturnsSearch();
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel result = await handler.Handle(
            new GetAutoTuneChannelMembers(AutoTuneAxis.TvGenre, "Nonexistent", 0, 100),
            CancellationToken.None);

        result.TotalCount.ShouldBe(0);
        result.Page.ShouldBeEmpty();
    }

    [Test]
    public async Task Handle_Should_Return_Empty_For_An_Out_Of_Range_Axis_Without_Searching()
    {
        // A crafted numeric axis (?axis=5) binds successfully; the handler must not let
        // GenerateQuery throw (which would surface as a 500) — it returns empty like a blank value.
        var handler = new GetAutoTuneChannelMembersHandler(_searchIndex, _db.Factory);

        PagedLibraryBrowseItemsResponseModel result = await handler.Handle(
            new GetAutoTuneChannelMembers((AutoTuneAxis)999, "Comedy", 0, 100),
            CancellationToken.None);

        result.TotalCount.ShouldBe(0);
        result.Page.ShouldBeEmpty();
        await _searchIndex.DidNotReceive().Search(
            Arg.Any<string>(),
            Arg.Any<string>(),
            Arg.Any<int>(),
            Arg.Any<int>(),
            Arg.Any<CancellationToken>());
    }

    private async Task SeedTwoShowGenreGraph()
    {
        await using TvContext context = _db.CreateContext();
        (LocalLibrary library, LibraryPath path) = MakeLibrary(1, "TV");

        var alpha = MakeShow(10, path, "Alpha Show");
        Season alphaSeason = MakeSeason(11, path, alpha);
        alphaSeason.Episodes.AddRange([
            MakeEpisode(101, path, alphaSeason),
            MakeEpisode(102, path, alphaSeason),
            MakeEpisode(103, path, alphaSeason)
        ]);
        alpha.Seasons.Add(alphaSeason);

        var beta = MakeShow(20, path, "Beta Show");
        Season betaSeason = MakeSeason(21, path, beta);
        betaSeason.Episodes.Add(MakeEpisode(201, path, betaSeason));
        beta.Seasons.Add(betaSeason);

        path.MediaItems.AddRange([
            alpha, beta, alphaSeason, betaSeason,
            .. alphaSeason.Episodes, .. betaSeason.Episodes
        ]);

        context.LocalLibraries.Add(library);
        context.Shows.AddRange(alpha, beta);
        context.Seasons.AddRange(alphaSeason, betaSeason);
        context.Episodes.AddRange([.. alphaSeason.Episodes, .. betaSeason.Episodes]);
        await context.SaveChangesAsync();
    }

    private async Task SeedMovieGenreGraph()
    {
        await using TvContext context = _db.CreateContext();
        (LocalLibrary library, LibraryPath path) = MakeLibrary(2, "Movies");
        var zebra = MakeMovie(30, path, "Zebra");
        var aardvark = MakeMovie(31, path, "Aardvark");
        path.MediaItems.AddRange([zebra, aardvark]);

        context.LocalLibraries.Add(library);
        context.Movies.AddRange(zebra, aardvark);
        await context.SaveChangesAsync();
    }

    private static (LocalLibrary Library, LibraryPath Path) MakeLibrary(int id, string name)
    {
        var library = new LocalLibrary
        {
            Id = id,
            Name = name,
            MediaKind = LibraryMediaKind.Movies,
            Paths = []
        };
        var path = new LibraryPath
        {
            Id = id,
            Path = $"/media/{id}",
            Library = library,
            LibraryFolders = [],
            MediaItems = []
        };
        library.Paths.Add(path);
        return (library, path);
    }

    private static Show MakeShow(int id, LibraryPath path, string title) =>
        new()
        {
            Id = id,
            LibraryPath = path,
            Collections = [],
            CollectionItems = [],
            TraktListItems = [],
            Seasons = [],
            ShowMetadata =
            [
                new ShowMetadata
                {
                    Title = title,
                    SortTitle = title,
                    Artwork = [],
                    Genres = [],
                    Tags = [],
                    Studios = [],
                    Actors = [],
                    Guids = [],
                    Subtitles = []
                }
            ]
        };

    private static Season MakeSeason(int id, LibraryPath path, Show show) =>
        new()
        {
            Id = id,
            LibraryPath = path,
            Show = show,
            SeasonNumber = 1,
            Collections = [],
            CollectionItems = [],
            TraktListItems = [],
            Episodes = [],
            SeasonMetadata = []
        };

    private static Episode MakeEpisode(int id, LibraryPath path, Season season) =>
        new()
        {
            Id = id,
            LibraryPath = path,
            Season = season,
            Collections = [],
            CollectionItems = [],
            TraktListItems = [],
            EpisodeMetadata = [],
            MediaVersions = []
        };

    private static Movie MakeMovie(int id, LibraryPath path, string title) =>
        new()
        {
            Id = id,
            LibraryPath = path,
            Collections = [],
            CollectionItems = [],
            TraktListItems = [],
            MediaVersions = [new MediaVersion { Duration = TimeSpan.FromMinutes(90) }],
            MovieMetadata =
            [
                new MovieMetadata
                {
                    Title = title,
                    SortTitle = title,
                    Artwork = [],
                    Genres = [],
                    Tags = [],
                    Studios = [],
                    Actors = [],
                    Guids = [],
                    Subtitles = [],
                    Directors = [],
                    Writers = []
                }
            ]
        };
}
