using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Core.Api.LibraryBrowse;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using MediatR;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Application.Channels;

// #425: per-source rotation weights turn an auto-tune channel from a single fair-share SmartCollection into
// a system-owned MultiCollection of per-source SmartCollections. CreateChannelFromLineup is mocked, so these
// verify the plan/persistence/ownership; end-to-end weighted round-robin is covered by
// WeightedShuffleCollectionEnumeratorTests + live-E2E.
[TestFixture]
public class CreateAutoTunedWeightedChannelsTests : ChannelHandlerTestBase
{
    private ISender _mediator = null!;
    private ISmartCollectionCache _smartCollectionCache = null!;
    private CreateChannelFromLineup _sentLineup;

    [SetUp]
    public void WeightedSetUp()
    {
        _mediator = Substitute.For<ISender>();
        _smartCollectionCache = Substitute.For<ISmartCollectionCache>();
        _sentLineup = null;

        _mediator.Send(Arg.Any<CreateChannelFromLineup>(), Arg.Any<CancellationToken>())
            .Returns(ci =>
            {
                _sentLineup = ci.Arg<CreateChannelFromLineup>();
                return (Either<BaseError, CreateChannelFromLineupResponseModel>)
                    new CreateChannelFromLineupResponseModel(88, null, 1, 2);
            });
    }

    private void SeedMembers(params (int Id, string Title)[] members) =>
        _mediator.Send(Arg.Any<GetAutoTuneChannelMembers>(), Arg.Any<CancellationToken>())
            .Returns(new PagedLibraryBrowseItemsResponseModel(
                members.Length,
                members.Select(m => Item(m.Id, m.Title)).ToList()));

    private static LibraryBrowseItemResponseModel Item(int id, string title) =>
        new(id, LibraryBrowseMediaType.TelevisionShow, title, null, null, string.Empty, null, null, null,
            CollectionType.SmartCollection, null, null, null, null, null, null);

    private CreateAutoTunedChannelsHandler MakeHandler() =>
        new(_mediator, Db.Factory, SearchTargets, _smartCollectionCache);

    private async Task<(MultiCollection Mc, List<SmartCollection> Members)> LoadOnlyMultiCollection()
    {
        await using TvContext context = Db.CreateContext();
        MultiCollection mc = await context.MultiCollections
            .Include(m => m.MultiCollectionSmartItems)
            .ThenInclude(i => i.SmartCollection)
            .SingleAsync();
        List<SmartCollection> members = mc.MultiCollectionSmartItems
            .OrderBy(i => i.SmartCollection.Name)
            .Select(i => i.SmartCollection)
            .ToList();
        return (mc, members);
    }

    [Test]
    public async Task TvGenre_Materializes_Every_Show_With_A_Live_Remainder()
    {
        SeedMembers((1, "Alpha"), (2, "Beta"), (3, "Gamma"));

        AutoTuneResult result = await MakeHandler().Handle(
            new CreateAutoTunedChannels(3, "Auto-Tuned", new List<AutoTuneChannelSelection>
            {
                new(AutoTuneAxis.TvGenre, "Comedy", "Comedy", "500",
                    Sources: [new AutoTuneSourceWeight(1, Weight: 3)])
            }),
            CancellationToken.None);

        result.CreatedCount.ShouldBe(1);

        (MultiCollection mc, List<SmartCollection> _) = await LoadOnlyMultiCollection();

        // 3 shows + 1 remainder, all owned by the created channel.
        mc.MultiCollectionSmartItems.Count.ShouldBe(4);
        mc.OwnedByChannelId.ShouldBe(88);

        Dictionary<string, int> weightByQuery = mc.MultiCollectionSmartItems
            .ToDictionary(i => i.SmartCollection.Query, i => i.Weight);

        weightByQuery["type:episode AND show_title:\"Alpha\""].ShouldBe(3);
        weightByQuery["type:episode AND show_title:\"Beta\""].ShouldBe(1);
        weightByQuery["type:episode AND show_title:\"Gamma\""].ShouldBe(1);
        weightByQuery[
                "(type:episode AND genre:\"Comedy\") AND NOT (show_title:\"Alpha\" OR show_title:\"Beta\" OR show_title:\"Gamma\")"]
            .ShouldBe(1);

        // The channel points at the MultiCollection with WeightedShuffle.
        _sentLineup.Advanced.PlaybackOrder.ShouldBe(PlaybackOrder.WeightedShuffle);
        _sentLineup.Lineup.Count.ShouldBe(1);
        _sentLineup.Lineup[0].CollectionType.ShouldBe(CollectionType.MultiCollection);
        _sentLineup.Lineup[0].MultiCollectionId.ShouldBe(mc.Id);

        // Every member smart collection is stamped as owned (hidden from user lists, cleaned on delete).
        mc.MultiCollectionSmartItems.ShouldAllBe(i => i.SmartCollection.OwnedByChannelId == 88);
    }

    [Test]
    public async Task Excluded_Show_Is_Not_A_Member_But_Is_Subtracted_From_The_Remainder()
    {
        SeedMembers((1, "Alpha"), (2, "Beta"), (3, "Gamma"));

        await MakeHandler().Handle(
            new CreateAutoTunedChannels(3, "Auto-Tuned", new List<AutoTuneChannelSelection>
            {
                new(AutoTuneAxis.TvGenre, "Comedy", "Comedy", "500",
                    Sources: [new AutoTuneSourceWeight(2, Excluded: true)])
            }),
            CancellationToken.None);

        (MultiCollection mc, List<SmartCollection> members) = await LoadOnlyMultiCollection();

        // Beta is dropped: 2 shows + 1 remainder.
        members.Select(m => m.Query).ShouldNotContain("type:episode AND show_title:\"Beta\"");
        mc.MultiCollectionSmartItems.Count(i => !i.SmartCollection.Query.StartsWith("(")).ShouldBe(2);

        // But Beta is still subtracted so its episodes don't leak into the remainder.
        string remainder = members.Single(m => m.Query.StartsWith("(")).Query;
        remainder.ShouldContain("show_title:\"Beta\"");
    }

    [Test]
    public async Task MovieGenre_Materializes_Only_Touched_Movies_With_A_Count_Weighted_Remainder()
    {
        // 4 movies; only movie 10 is re-weighted. The untouched three stay in one count-weighted remainder.
        _mediator.Send(Arg.Any<GetAutoTuneChannelMembers>(), Arg.Any<CancellationToken>())
            .Returns(new PagedLibraryBrowseItemsResponseModel(4, new List<LibraryBrowseItemResponseModel>
            {
                MovieItem(10), MovieItem(11), MovieItem(12), MovieItem(13)
            }));

        await MakeHandler().Handle(
            new CreateAutoTunedChannels(3, "Auto-Tuned", new List<AutoTuneChannelSelection>
            {
                new(AutoTuneAxis.MovieGenre, "Action", "Action Movies", "500",
                    Sources: [new AutoTuneSourceWeight(10, Weight: 3)])
            }),
            CancellationToken.None);

        (MultiCollection mc, List<SmartCollection> members) = await LoadOnlyMultiCollection();

        // one materialized movie + one remainder
        mc.MultiCollectionSmartItems.Count.ShouldBe(2);

        MultiCollectionSmartItem movie = mc.MultiCollectionSmartItems
            .Single(i => i.SmartCollection.Query == "type:movie AND id:10");
        movie.Weight.ShouldBe(3);

        // remainder weight = the 3 un-touched base movies; subtracts the materialized movie by id
        MultiCollectionSmartItem remainder = mc.MultiCollectionSmartItems
            .Single(i => i.SmartCollection.Query.StartsWith("("));
        remainder.Weight.ShouldBe(3);
        remainder.SmartCollection.Query.ShouldBe("(type:movie AND genre:\"Action\") AND NOT (id:10)");
    }

    [Test]
    public async Task All_Default_Weights_Fall_Back_To_A_Single_SmartCollection()
    {
        SeedMembers((1, "Alpha"), (2, "Beta"));

        // Sources present but every weight is the fair-share default and nothing is excluded/added: the
        // channel keeps the cheap single-SmartCollection shape (no MultiCollection created).
        _mediator.Send(Arg.Any<CreateSmartCollection>(), Arg.Any<CancellationToken>())
            .Returns(ci => (Either<BaseError, SmartCollectionViewModel>)
                new SmartCollectionViewModel(7, ci.Arg<CreateSmartCollection>().Name,
                    ci.Arg<CreateSmartCollection>().Query));

        AutoTuneResult result = await MakeHandler().Handle(
            new CreateAutoTunedChannels(3, "Auto-Tuned", new List<AutoTuneChannelSelection>
            {
                new(AutoTuneAxis.TvGenre, "Comedy", "Comedy", "500",
                    Sources:
                    [
                        new AutoTuneSourceWeight(1),
                        new AutoTuneSourceWeight(2)
                    ])
            }),
            CancellationToken.None);

        result.CreatedCount.ShouldBe(1);

        await using TvContext context = Db.CreateContext();
        (await context.MultiCollections.AnyAsync()).ShouldBeFalse();

        _sentLineup.Lineup[0].CollectionType.ShouldBe(CollectionType.SmartCollection);
        _sentLineup.Lineup[0].SmartCollectionId.ShouldBe(7);
    }

    private static LibraryBrowseItemResponseModel MovieItem(int id) =>
        new(id, LibraryBrowseMediaType.Movie, $"Movie {id}", null, null, string.Empty, null, null, null,
            CollectionType.SmartCollection, null, null, null, null, null, null);
}
