using System.IO.Abstractions;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Libraries;
using ErsatzTV.Core;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Search;
using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Tests.Support;
using LanguageExt;
using Microsoft.EntityFrameworkCore;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using Testably.Abstractions.Testing;
using ThreadingChannel = System.Threading.Channels.Channel;

namespace ErsatzTV.Tests.Application.Libraries;

[TestFixture]
public class UpdateLocalLibraryHandlerTests
{
    private InMemoryTvContext _db = null!;

    [SetUp]
    public async Task SetUp() => _db = await InMemoryTvContext.CreateAsync();

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

    [Test]
    public async Task Handle_Should_Return_422_When_New_Path_Does_Not_Exist()
    {
        (int libraryId, int pathId) = await SeedLibraryWithPath("Movies", "/media/movies");

        UpdateLocalLibraryHandler handler = CreateHandler(new MockFileSystem());

        Either<BaseError, LocalLibraryViewModel> result = await handler.Handle(
            new UpdateLocalLibrary(
                libraryId,
                "Movies",
                [
                    new UpdateLocalLibraryPath(pathId, "/media/movies"),
                    new UpdateLocalLibraryPath(0, "/media/missing")
                ]),
            CancellationToken.None);

        result.IsLeft.ShouldBeTrue();
        result.IfLeft(error => error.Value.ShouldContain("/media/missing"));
    }

    [Test]
    public async Task Handle_Should_Exempt_Existing_Paths_From_The_Existence_Check()
    {
        // the existing row's directory is NOT in the mock filesystem (simulates an unmounted
        // share) -- design #202 §C2: existing rows are exempt so a rename doesn't fail on a
        // temporarily-missing mount.
        (int libraryId, int pathId) = await SeedLibraryWithPath("Movies", "/media/movies");

        UpdateLocalLibraryHandler handler = CreateHandler(new MockFileSystem());

        Either<BaseError, LocalLibraryViewModel> result = await handler.Handle(
            new UpdateLocalLibrary(
                libraryId,
                "Movies Renamed",
                [new UpdateLocalLibraryPath(pathId, "/media/movies")]),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue();
    }

    [Test]
    public async Task Handle_Should_Identify_Paths_By_Normalized_String_Not_Id()
    {
        (int libraryId, int pathId) = await SeedLibraryWithPath("Movies", "/media/Movies/");

        var fileSystem = new MockFileSystem();
        fileSystem.Directory.CreateDirectory("/media/movies");

        UpdateLocalLibraryHandler handler = CreateHandler(fileSystem);

        // same path, different case + no trailing slash, Id omitted (0): the handler's merge is by
        // normalized path string (design #202 §C4c), so this must NOT be treated as delete-old +
        // add-new -- the existing LibraryPath row (and its id) is preserved.
        Either<BaseError, LocalLibraryViewModel> result = await handler.Handle(
            new UpdateLocalLibrary(libraryId, "Movies", [new UpdateLocalLibraryPath(0, "/media/movies")]),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue();

        await using TvContext dbContext = _db.CreateContext();
        List<LibraryPath> paths = await dbContext.LibraryPaths
            .Where(lp => lp.LibraryId == libraryId)
            .ToListAsync();

        paths.Count.ShouldBe(1);
        paths[0].Id.ShouldBe(pathId);
    }

    [Test]
    public async Task Handle_Should_Add_And_Remove_Paths_By_Normalized_String_Diff()
    {
        (int libraryId, int keptPathId) = await SeedLibraryWithPath("Movies", "/media/keep");

        var fileSystem = new MockFileSystem();
        fileSystem.Directory.CreateDirectory("/media/new");

        UpdateLocalLibraryHandler handler = CreateHandler(fileSystem);

        Either<BaseError, LocalLibraryViewModel> result = await handler.Handle(
            new UpdateLocalLibrary(
                libraryId,
                "Movies",
                [
                    new UpdateLocalLibraryPath(keptPathId, "/media/keep"),
                    new UpdateLocalLibraryPath(0, "/media/new")
                ]),
            CancellationToken.None);

        result.IsRight.ShouldBeTrue();

        await using TvContext dbContext = _db.CreateContext();
        List<string> paths = await dbContext.LibraryPaths
            .Where(lp => lp.LibraryId == libraryId)
            .Select(lp => lp.Path)
            .ToListAsync();

        paths.Count.ShouldBe(2);
        paths.ShouldContain("/media/keep");
        paths.ShouldContain("/media/new");
    }

    private UpdateLocalLibraryHandler CreateHandler(IFileSystem fileSystem) =>
        new(
            ThreadingChannel.CreateUnbounded<IScannerBackgroundServiceRequest>().Writer,
            Substitute.For<IEntityLocker>(),
            fileSystem,
            Substitute.For<ISearchIndex>(),
            _db.Factory);

    private async Task<(int LibraryId, int PathId)> SeedLibraryWithPath(string name, string path)
    {
        await using TvContext context = _db.CreateContext();
        var source = new LocalMediaSource
        {
            Libraries =
            [
                new LocalLibrary
                {
                    Name = name,
                    MediaKind = LibraryMediaKind.Movies,
                    Paths = [new LibraryPath { Path = path }]
                }
            ]
        };
        await context.LocalMediaSources.AddAsync(source);
        await context.SaveChangesAsync();
        return (source.Libraries[0].Id, source.Libraries[0].Paths[0].Id);
    }
}
