using System.Reflection;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.Emby;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaSources;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Emby;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Locking;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Routing;
using NSubstitute;
using NUnit.Framework;
using Shouldly;
using static LanguageExt.Prelude;
using Unit = LanguageExt.Unit;

namespace ErsatzTV.Tests.Controllers;

[TestFixture]
public class EmbyMediaSourcesControllerTests
{
    private IMediator _mediator = null!;
    private IEntityLocker _entityLocker = null!;
    private Channel<IScannerBackgroundServiceRequest> _scannerChannel = null!;
    private EmbyMediaSourcesController _controller = null!;

    [SetUp]
    public void SetUp()
    {
        _mediator = Substitute.For<IMediator>();
        _entityLocker = Substitute.For<IEntityLocker>();
        _scannerChannel = System.Threading.Channels.Channel.CreateUnbounded<IScannerBackgroundServiceRequest>();
        _controller = new EmbyMediaSourcesController(_mediator, _entityLocker, _scannerChannel.Writer);
    }

    [Test]
    public void Controller_Should_Expose_Idiomatic_Rest_Routes()
    {
        ShouldHaveActionRoute(nameof(EmbyMediaSourcesController.GetState), "GET", "/api/v1/media-sources/emby");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.GetConnection),
            "GET",
            "/api/v1/media-sources/emby/connection");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.SaveConnection),
            "PUT",
            "/api/v1/media-sources/emby/connection");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.Disconnect),
            "POST",
            "/api/v1/media-sources/emby/disconnect");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.GetLibraries),
            "GET",
            "/api/v1/media-sources/emby/{id:int}/libraries");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.ReplaceLibraryPreferences),
            "PUT",
            "/api/v1/media-sources/emby/{id:int}/libraries");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.GetPathReplacements),
            "GET",
            "/api/v1/media-sources/emby/{id:int}/path-replacements");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.ReplacePathReplacements),
            "PUT",
            "/api/v1/media-sources/emby/{id:int}/path-replacements");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.RefreshLibraries),
            "POST",
            "/api/v1/media-sources/emby/{id:int}/refresh-libraries");
        ShouldHaveActionRoute(
            nameof(EmbyMediaSourcesController.ScanCollections),
            "POST",
            "/api/v1/media-sources/emby/{id:int}/scan-collections");
    }

    [Test]
    public async Task GetState_Should_Report_Authorized_And_Locked()
    {
        _mediator.Send(Arg.Any<GetAllEmbyMediaSources>(), Arg.Any<CancellationToken>())
            .Returns([new EmbyMediaSourceViewModel(1, "My Server", "http://emby.local")]);
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(new EmbySecrets { Address = "http://emby.local", ApiKey = "secret" });
        _entityLocker.IsRemoteMediaSourceLocked<EmbyMediaSource>().Returns(true);

        RemoteMediaSourceStateResponseModel result = await _controller.GetState(CancellationToken.None);

        result.IsAuthorized.ShouldBeTrue();
        result.IsLocked.ShouldBeTrue();
        result.Servers.ShouldBe([new RemoteMediaSourceItemResponseModel(1, "My Server", "http://emby.local")]);
    }

    [Test]
    public async Task GetState_Should_Report_Unauthorized_When_ApiKey_Blank()
    {
        _mediator.Send(Arg.Any<GetAllEmbyMediaSources>(), Arg.Any<CancellationToken>())
            .Returns(new List<EmbyMediaSourceViewModel>());
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(new EmbySecrets { Address = "http://emby.local", ApiKey = "" });

        RemoteMediaSourceStateResponseModel result = await _controller.GetState(CancellationToken.None);

        result.IsAuthorized.ShouldBeFalse();
    }

    [Test]
    public async Task GetConnection_Should_Never_Return_The_Api_Key()
    {
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(new EmbySecrets { Address = "http://emby.local", ApiKey = "super-secret" });

        RemoteConnectionResponseModel result = await _controller.GetConnection(CancellationToken.None);

        result.Address.ShouldBe("http://emby.local");
        result.HasApiKey.ShouldBeTrue();
        result.ToString().ShouldNotContain("super-secret");
    }

    [Test]
    public async Task GetConnection_Should_Report_No_Key_When_Not_Configured()
    {
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(new EmbySecrets { Address = "", ApiKey = "" });

        RemoteConnectionResponseModel result = await _controller.GetConnection(CancellationToken.None);

        result.HasApiKey.ShouldBeFalse();
    }

    [Test]
    public async Task SaveConnection_Should_Return_409_When_Locked()
    {
        _entityLocker.IsRemoteMediaSourceLocked<EmbyMediaSource>().Returns(true);

        IActionResult result = await _controller.SaveConnection(
            new SaveRemoteConnectionRequest("http://emby.local", "key"),
            CancellationToken.None);

        result.ShouldBeOfType<ConflictObjectResult>().StatusCode.ShouldBe(409);
        await _mediator.DidNotReceive().Send(Arg.Any<SaveEmbySecrets>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task SaveConnection_Should_Return_422_For_Non_Absolute_Address()
    {
        IActionResult result = await _controller.SaveConnection(
            new SaveRemoteConnectionRequest("not-a-uri", "key"),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
        await _mediator.DidNotReceive().Send(Arg.Any<SaveEmbySecrets>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task SaveConnection_Should_Return_422_When_Key_Blank_On_First_Connect()
    {
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(new EmbySecrets { Address = "", ApiKey = "" });

        IActionResult result = await _controller.SaveConnection(
            new SaveRemoteConnectionRequest("http://emby.local", ""),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
        await _mediator.DidNotReceive().Send(Arg.Any<SaveEmbySecrets>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task SaveConnection_Should_Retain_Existing_Key_When_Blank()
    {
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(
                new EmbySecrets { Address = "http://old.local", ApiKey = "existing-key" },
                new EmbySecrets { Address = "http://emby.local", ApiKey = "existing-key" });
        _mediator.Send(Arg.Any<SaveEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));

        IActionResult result = await _controller.SaveConnection(
            new SaveRemoteConnectionRequest("http://emby.local", "   "),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<RemoteConnectionResponseModel>()
            .HasApiKey.ShouldBeTrue();
        await _mediator.Received(1).Send(
            Arg.Is<SaveEmbySecrets>(c => c.Secrets.ApiKey == "existing-key" && c.Secrets.Address == "http://emby.local"),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task SaveConnection_Should_Set_New_Key_When_NonBlank()
    {
        _mediator.Send(Arg.Any<GetEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(
                new EmbySecrets { Address = "http://old.local", ApiKey = "existing-key" },
                new EmbySecrets { Address = "http://emby.local", ApiKey = "new-key" });
        _mediator.Send(Arg.Any<SaveEmbySecrets>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));

        IActionResult result = await _controller.SaveConnection(
            new SaveRemoteConnectionRequest("http://emby.local", "new-key"),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>();
        await _mediator.Received(1).Send(
            Arg.Is<SaveEmbySecrets>(c => c.Secrets.ApiKey == "new-key"),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Disconnect_Should_Return_409_When_Lock_Fails()
    {
        _entityLocker.LockRemoteMediaSource<EmbyMediaSource>().Returns(false);

        IActionResult result = await _controller.Disconnect(CancellationToken.None);

        result.ShouldBeOfType<ConflictObjectResult>().StatusCode.ShouldBe(409);
        await _mediator.DidNotReceive().Send(Arg.Any<DisconnectEmby>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Disconnect_Should_Return_204_On_Success()
    {
        _entityLocker.LockRemoteMediaSource<EmbyMediaSource>().Returns(true);
        _mediator.Send(Arg.Any<DisconnectEmby>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));

        IActionResult result = await _controller.Disconnect(CancellationToken.None);

        result.ShouldBeOfType<NoContentResult>();
    }

    [Test]
    public async Task GetLibraries_Should_Return_404_When_Source_Missing()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.None);

        IActionResult result = await _controller.GetLibraries(99, CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task ReplaceLibraryPreferences_Should_Return_422_For_Foreign_Id()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<GetEmbyLibrariesBySourceId>(), Arg.Any<CancellationToken>())
            .Returns([new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1)]);

        IActionResult result = await _controller.ReplaceLibraryPreferences(
            1,
            new ReplaceRemoteLibraryPreferencesRequest([new RemoteLibraryPreferenceRequest(999, true)]),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
        await _mediator.DidNotReceive().Send(Arg.Any<UpdateEmbyLibraryPreferences>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task ReplaceLibraryPreferences_Should_Return_422_When_Not_Covering_All_Libraries()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<GetEmbyLibrariesBySourceId>(), Arg.Any<CancellationToken>())
            .Returns(
                [
                    new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1),
                    new EmbyLibraryViewModel(2, "Shows", LibraryMediaKind.Shows, true, 1)
                ]);

        IActionResult result = await _controller.ReplaceLibraryPreferences(
            1,
            new ReplaceRemoteLibraryPreferencesRequest([new RemoteLibraryPreferenceRequest(1, true)]),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
    }

    [Test]
    public async Task ReplaceLibraryPreferences_Should_Enqueue_Sync_Pair_For_Enabled_Libraries_And_Return_Reloaded()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<GetEmbyLibrariesBySourceId>(), Arg.Any<CancellationToken>())
            .Returns(
                [new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1)],
                [new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1)]);
        _mediator.Send(Arg.Any<UpdateEmbyLibraryPreferences>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _entityLocker.LockLibrary(1).Returns(true);

        IActionResult result = await _controller.ReplaceLibraryPreferences(
            1,
            new ReplaceRemoteLibraryPreferencesRequest([new RemoteLibraryPreferenceRequest(1, true)]),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<List<RemoteLibraryResponseModel>>()
            .Single().Id.ShouldBe(1);

        _scannerChannel.Reader.TryRead(out IScannerBackgroundServiceRequest? first).ShouldBeTrue();
        first.ShouldBeOfType<SynchronizeEmbyLibraries>().EmbyMediaSourceId.ShouldBe(1);
        _scannerChannel.Reader.TryRead(out IScannerBackgroundServiceRequest? second).ShouldBeTrue();
        second.ShouldBeOfType<SynchronizeEmbyLibraryByIdIfNeeded>().EmbyLibraryId.ShouldBe(1);
    }

    [Test]
    public async Task ReplaceLibraryPreferences_Should_Skip_Enqueue_When_Library_Locked()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<GetEmbyLibrariesBySourceId>(), Arg.Any<CancellationToken>())
            .Returns(
                [new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1)],
                [new EmbyLibraryViewModel(1, "Movies", LibraryMediaKind.Movies, true, 1)]);
        _mediator.Send(Arg.Any<UpdateEmbyLibraryPreferences>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _entityLocker.LockLibrary(1).Returns(false);

        await _controller.ReplaceLibraryPreferences(
            1,
            new ReplaceRemoteLibraryPreferencesRequest([new RemoteLibraryPreferenceRequest(1, true)]),
            CancellationToken.None);

        _scannerChannel.Reader.TryRead(out _).ShouldBeFalse();
    }

    [Test]
    public async Task GetPathReplacements_Should_Return_404_When_Source_Missing()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.None);

        IActionResult result = await _controller.GetPathReplacements(99, CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task ReplacePathReplacements_Should_Return_200_With_Reloaded_List()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<UpdateEmbyPathReplacements>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetEmbyPathReplacementsBySourceId>(), Arg.Any<CancellationToken>())
            .Returns([new EmbyPathReplacementViewModel(1, "/emby", "/local")]);

        IActionResult result = await _controller.ReplacePathReplacements(
            1,
            new ReplacePathReplacementsRequest([new PathReplacementItemRequest(1, "/emby", "/local")]),
            CancellationToken.None);

        List<PathReplacementResponseModel> body = result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<List<PathReplacementResponseModel>>();
        body.Single().ShouldBe(new PathReplacementResponseModel(1, "/emby", "/local"));
    }

    [Test]
    public async Task ReplacePathReplacements_Should_Map_Handler_Error_To_422()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _mediator.Send(Arg.Any<UpdateEmbyPathReplacements>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, Unit>(BaseError.New("bad row")));

        IActionResult result = await _controller.ReplacePathReplacements(
            1,
            new ReplacePathReplacementsRequest([new PathReplacementItemRequest(999, "", "")]),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
    }

    [Test]
    public async Task RefreshLibraries_Should_Return_404_When_Source_Missing()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.None);

        IActionResult result = await _controller.RefreshLibraries(99, CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task RefreshLibraries_Should_Return_409_When_Locked()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _entityLocker.IsRemoteMediaSourceLocked<EmbyMediaSource>().Returns(true);

        IActionResult result = await _controller.RefreshLibraries(1, CancellationToken.None);

        result.ShouldBeOfType<ConflictObjectResult>().StatusCode.ShouldBe(409);
    }

    [Test]
    public async Task RefreshLibraries_Should_Enqueue_And_Return_202()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _entityLocker.IsRemoteMediaSourceLocked<EmbyMediaSource>().Returns(false);

        IActionResult result = await _controller.RefreshLibraries(1, CancellationToken.None);

        result.ShouldBeOfType<AcceptedResult>();
        _scannerChannel.Reader.TryRead(out IScannerBackgroundServiceRequest? request).ShouldBeTrue();
        request.ShouldBeOfType<SynchronizeEmbyLibraries>().EmbyMediaSourceId.ShouldBe(1);
    }

    [Test]
    public async Task ScanCollections_Should_Return_404_When_Source_Missing()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.None);

        IActionResult result = await _controller.ScanCollections(99, cancellationToken: CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
        _entityLocker.DidNotReceive().LockEmbyCollections();
    }

    [Test]
    public async Task ScanCollections_Should_Return_409_When_Collections_Locked()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _entityLocker.LockEmbyCollections().Returns(false);

        IActionResult result = await _controller.ScanCollections(1, cancellationToken: CancellationToken.None);

        result.ShouldBeOfType<ConflictObjectResult>().Value.ShouldBeOfType<ProblemDetails>().Status.ShouldBe(409);
        _scannerChannel.Reader.TryRead(out _).ShouldBeFalse();
    }

    [Test]
    public async Task ScanCollections_Should_Enqueue_And_Return_202()
    {
        _mediator.Send(Arg.Any<GetEmbyMediaSourceById>(), Arg.Any<CancellationToken>())
            .Returns(Option<EmbyMediaSourceViewModel>.Some(new EmbyMediaSourceViewModel(1, "s", "a")));
        _entityLocker.LockEmbyCollections().Returns(true);

        IActionResult result = await _controller.ScanCollections(1, deep: true, CancellationToken.None);

        result.ShouldBeOfType<AcceptedResult>();
        _scannerChannel.Reader.TryRead(out IScannerBackgroundServiceRequest? request).ShouldBeTrue();
        var command = request.ShouldBeOfType<SynchronizeEmbyCollections>();
        command.EmbyMediaSourceId.ShouldBe(1);
        command.ForceScan.ShouldBeTrue();
        command.DeepScan.ShouldBeTrue();
    }

    private static void ShouldHaveActionRoute(string actionName, string httpMethod, string route)
    {
        MethodInfo action = typeof(EmbyMediaSourcesController).GetMethod(actionName)
            ?? throw new AssertionException($"Missing action {actionName}");

        HttpMethodAttribute attribute = action.GetCustomAttributes<HttpMethodAttribute>(inherit: true).Single();
        attribute.HttpMethods.ShouldContain(httpMethod);
        attribute.Template.ShouldBe(route);
    }
}
