using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Trakt;
using ErsatzTV.Core.Errors;
using ErsatzTV.Core.Interfaces.Locking;
using LanguageExt;
using static LanguageExt.Prelude;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Controllers;

[TestFixture]
public class TraktControllerTests
{
    private IMediator _mediator = null!;
    private IEntityLocker _entityLocker = null!;
    private Channel<IBackgroundServiceRequest> _workerChannel = null!;
    private TraktController _controller = null!;

    [SetUp]
    public void SetUp()
    {
        _mediator = Substitute.For<IMediator>();
        _entityLocker = Substitute.For<IEntityLocker>();
        _workerChannel = Channel.CreateUnbounded<IBackgroundServiceRequest>();
        _controller = new TraktController(_mediator, _workerChannel.Writer, _entityLocker);
    }

    [Test]
    public async Task GetAll_Should_Clamp_Paging_And_Project_Response()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, false);
        _mediator.Send(Arg.Any<GetPagedTraktLists>(), Arg.Any<CancellationToken>())
            .Returns(new PagedTraktListsViewModel(1, [vm]));

        PagedTraktListsResponseModel result = await _controller.GetAll(-5, 500, CancellationToken.None);

        result.TotalCount.ShouldBe(1);
        result.Page.ShouldBe(
            [new TraktListResponseModel(1, 100, "my-list", "My List", 10, 8, true, false)]);
        await _mediator.Received(1).Send(
            Arg.Is<GetPagedTraktLists>(q => q.PageNum == 0 && q.PageSize == 100),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetById_Should_Return_200_For_Some()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));

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

        result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBe(new TraktListResponseModel(1, 100, "my-list", "My List", 10, 8, true, false));
    }

    [Test]
    public async Task GetById_Should_Return_404_For_None()
    {
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.None);

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

        var notFound = result.ShouldBeOfType<NotFoundObjectResult>();
        var problemDetails = notFound.Value.ShouldBeOfType<ProblemDetails>();
        problemDetails.Status.ShouldBe(404);
    }

    [Test]
    public async Task Add_Should_Return_422_For_Invalid_Url()
    {
        IActionResult result = await _controller.Add(
            new AddTraktListRequest("not-a-trakt-url ?? "),
            CancellationToken.None);

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

    [TestCase("https://trakt.tv/users/someuser/lists/some-list")]
    [TestCase("https://app.trakt.tv/users/someuser/some-list")]
    [TestCase("https://trakt.tv/lists/someuser/some-list")]
    [TestCase("someuser/lists/some-list")]
    [TestCase("someuser/some-list")]
    public async Task Add_Should_Accept_Known_Trakt_Url_Shapes_And_Dispatch(string url)
    {
        _entityLocker.LockTrakt().Returns(true);

        IActionResult result = await _controller.Add(new AddTraktListRequest(url), CancellationToken.None);

        result.ShouldBeOfType<AcceptedResult>();
        _workerChannel.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
        request.ShouldBeOfType<AddTraktList>().TraktListUrl.ShouldBe(url);
    }

    [Test]
    public async Task Add_Should_Return_409_When_Already_Locked()
    {
        _entityLocker.LockTrakt().Returns(false);

        IActionResult result = await _controller.Add(
            new AddTraktListRequest("https://trakt.tv/users/someuser/lists/some-list"),
            CancellationToken.None);

        result.ShouldBeOfType<ConflictObjectResult>().StatusCode.ShouldBe(409);
        _workerChannel.Reader.TryRead(out _).ShouldBeFalse();
    }

    [Test]
    public async Task Match_Should_Return_404_When_List_Missing()
    {
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.None);

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

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

    [Test]
    public async Task Match_Should_Dispatch_And_Return_202()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));
        _entityLocker.LockTrakt().Returns(true);

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

        result.ShouldBeOfType<AcceptedResult>();
        _workerChannel.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
        request.ShouldBeOfType<MatchTraktListItems>().TraktListId.ShouldBe(1);
    }

    [Test]
    public async Task Match_Should_Return_409_When_Already_Locked()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));
        _entityLocker.LockTrakt().Returns(false);

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

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

    [Test]
    public async Task Delete_Should_Return_404_When_List_Missing()
    {
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.None);

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

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Delete_Should_Dispatch_And_Return_202()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));
        _entityLocker.LockTrakt().Returns(true);

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

        result.ShouldBeOfType<AcceptedResult>();
        _workerChannel.Reader.TryRead(out IBackgroundServiceRequest? request).ShouldBeTrue();
        request.ShouldBeOfType<DeleteTraktList>().TraktListId.ShouldBe(1);
    }

    [Test]
    public async Task Update_Should_Return_404_When_List_Missing()
    {
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.None);

        IActionResult result = await _controller.Update(
            99,
            new UpdateTraktListRequest(true, false),
            CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
        await _mediator.DidNotReceive().Send(Arg.Any<UpdateTraktList>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Update_Should_Return_200_With_Updated_List()
    {
        var before = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, false, false);
        var after = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, true, true);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(before), Option<TraktListViewModel>.Some(after));
        _mediator.Send(Arg.Any<UpdateTraktList>(), Arg.Any<CancellationToken>())
            .Returns(Option<BaseError>.None);

        IActionResult result = await _controller.Update(
            1,
            new UpdateTraktListRequest(true, true),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBe(new TraktListResponseModel(1, 100, "my-list", "My List", 10, 8, true, true));
        await _mediator.Received(1).Send(
            Arg.Is<UpdateTraktList>(c => c.Id == 1 && c.AutoRefresh && c.GeneratePlaylist),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Update_Should_Return_422_For_Error()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, false, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));
        _mediator.Send(Arg.Any<UpdateTraktList>(), Arg.Any<CancellationToken>())
            .Returns(Option<BaseError>.Some(BaseError.New("bad")));

        IActionResult result = await _controller.Update(
            1,
            new UpdateTraktListRequest(true, true),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task Update_Should_Return_404_For_NotFoundError()
    {
        var vm = new TraktListViewModel(1, 100, "my-list", "My List", 10, 8, false, false);
        _mediator.Send(Arg.Any<GetTraktListById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TraktListViewModel>.Some(vm));
        _mediator.Send(Arg.Any<UpdateTraktList>(), Arg.Any<CancellationToken>())
            .Returns(Option<BaseError>.Some(new NotFoundError("missing")));

        IActionResult result = await _controller.Update(
            1,
            new UpdateTraktListRequest(true, true),
            CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [TestCase(true)]
    [TestCase(false)]
    public void GetStatus_Should_Wrap_IsTraktLocked(bool busy)
    {
        _entityLocker.IsTraktLocked().Returns(busy);

        TraktStatusResponseModel result = _controller.GetStatus();

        result.Busy.ShouldBe(busy);
    }
}
