using System.Reflection;
using ErsatzTV.Application.MediaCollections;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.MediaCollections;
using ErsatzTV.Core.Domain;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Http;
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 RerunCollectionControllerTests
{
    [SetUp]
    public void SetUp()
    {
        _mediator = Substitute.For<IMediator>();
        _controller = new RerunCollectionController(_mediator)
        {
            // Real HttpContext so the #253 ETag/If-Match concurrency headers can be read/written.
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
        };
    }

    private RerunCollectionController _controller = null!;
    private IMediator _mediator = null!;

    [Test]
    public void Controller_Should_Expose_Idiomatic_Rest_Routes()
    {
        ShouldHaveActionRoute(nameof(RerunCollectionController.GetAll), "GET", "/api/v1/rerun-collections");
        ShouldHaveActionRoute(nameof(RerunCollectionController.GetById), "GET", "/api/v1/rerun-collections/{id:int}");
        ShouldHaveActionRoute(nameof(RerunCollectionController.Create), "POST", "/api/v1/rerun-collections");
        ShouldHaveActionRoute(nameof(RerunCollectionController.Update), "PUT", "/api/v1/rerun-collections/{id:int}");
        ShouldHaveActionRoute(nameof(RerunCollectionController.Delete), "DELETE", "/api/v1/rerun-collections/{id:int}");
    }

    [Test]
    public async Task GetAll_Should_Clamp_Paging_And_Map_Response()
    {
        _mediator.Send(Arg.Any<GetPagedRerunCollections>(), Arg.Any<CancellationToken>())
            .Returns(new PagedRerunCollectionsViewModel(
                1,
                new List<RerunCollectionViewModel> { MakeRerunFromSmart(3, "Nightly") }));

        PagedRerunCollectionsResponseModel result =
            await _controller.GetAll("n", -5, 9999, CancellationToken.None);

        result.TotalCount.ShouldBe(1);
        result.Page.Count.ShouldBe(1);
        result.Page[0].Id.ShouldBe(3);
        result.Page[0].CollectionType.ShouldBe(CollectionType.SmartCollection);
        result.Page[0].SelectedId.ShouldBe(20);
        result.Page[0].SelectedName.ShouldBe("Smart");
        await _mediator.Received(1).Send(
            Arg.Is<GetPagedRerunCollections>(q => q.Query == "n" && q.PageNum == 0 && q.PageSize == 100),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetById_Should_Return_200_For_Some()
    {
        _mediator.Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>())
            .Returns(Option<RerunCollectionViewModel>.Some(MakeRerunFromSmart(4, "Nightly")));

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

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<RerunCollectionResponseModel>().Id.ShouldBe(4);
    }

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

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

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Create_Should_Return_201_And_Resolve_Selected_Id_Into_Collection_Vm()
    {
        _mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, RerunCollectionViewModel>(MakeRerunFromSmart(8, "Nightly")));

        IActionResult result = await _controller.Create(
            new CreateRerunCollectionRequest(
                "Nightly",
                CollectionType.Collection,
                42,
                PlaybackOrder.Chronological,
                PlaybackOrder.Shuffle),
            CancellationToken.None);

        var created = result.ShouldBeOfType<CreatedResult>();
        created.Location.ShouldBe("/api/v1/rerun-collections/8");
        await _mediator.Received(1).Send(
            Arg.Is<CreateRerunCollection>(c =>
                c.Name == "Nightly" &&
                c.CollectionType == CollectionType.Collection &&
                c.Collection != null && c.Collection.Id == 42 &&
                c.MultiCollection == null &&
                c.SmartCollection == null &&
                c.MediaItem == null &&
                c.FirstRunPlaybackOrder == PlaybackOrder.Chronological &&
                c.RerunPlaybackOrder == PlaybackOrder.Shuffle),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Create_Should_Resolve_Media_Item_Selection_For_Show_Type()
    {
        _mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, RerunCollectionViewModel>(MakeRerunFromSmart(9, "Nightly")));

        await _controller.Create(
            new CreateRerunCollectionRequest(
                "Nightly",
                CollectionType.TelevisionShow,
                77,
                PlaybackOrder.Chronological,
                PlaybackOrder.Chronological),
            CancellationToken.None);

        await _mediator.Received(1).Send(
            Arg.Is<CreateRerunCollection>(c =>
                c.CollectionType == CollectionType.TelevisionShow &&
                c.Collection == null &&
                c.MediaItem != null && c.MediaItem.MediaItemId == 77),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Create_Should_Return_422_On_Validation_Error()
    {
        _mediator.Send(Arg.Any<CreateRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, RerunCollectionViewModel>(BaseError.New("dupe")));

        IActionResult result = await _controller.Create(
            new CreateRerunCollectionRequest(
                "Nightly",
                CollectionType.SmartCollection,
                1,
                PlaybackOrder.Chronological,
                PlaybackOrder.Chronological),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task Create_Should_Return_422_For_Unsupported_Collection_Type()
    {
        IActionResult result = await _controller.Create(
            new CreateRerunCollectionRequest(
                "Nightly",
                CollectionType.Playlist,
                1,
                PlaybackOrder.Chronological,
                PlaybackOrder.Chronological),
            CancellationToken.None);

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

    [Test]
    public async Task Update_Should_Return_200_With_Refreshed_Body_And_Resolve_Selection()
    {
        _mediator.Send(Arg.Any<UpdateRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetRerunCollectionById>(), Arg.Any<CancellationToken>())
            .Returns(Option<RerunCollectionViewModel>.Some(MakeRerunFromSmart(4, "Nightly")));

        IActionResult result = await _controller.Update(
            4,
            new UpdateRerunCollectionRequest(
                "Nightly",
                CollectionType.MultiCollection,
                55,
                PlaybackOrder.Chronological,
                PlaybackOrder.Shuffle),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<RerunCollectionResponseModel>().Id.ShouldBe(4);
        await _mediator.Received(1).Send(
            Arg.Is<UpdateRerunCollection>(c =>
                c.RerunCollectionId == 4 &&
                c.Name == "Nightly" &&
                c.CollectionType == CollectionType.MultiCollection &&
                c.MultiCollection != null && c.MultiCollection.Id == 55 &&
                c.Collection == null &&
                c.SmartCollection == null &&
                c.MediaItem == null),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Update_Should_Return_422_On_Validation_Error()
    {
        _mediator.Send(Arg.Any<UpdateRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, Unit>(BaseError.New("bad")));

        IActionResult result = await _controller.Update(
            4,
            new UpdateRerunCollectionRequest(
                "Nightly",
                CollectionType.SmartCollection,
                1,
                PlaybackOrder.Chronological,
                PlaybackOrder.Chronological),
            CancellationToken.None);

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

    [Test]
    public async Task Update_Should_Return_422_For_Unsupported_Collection_Type()
    {
        IActionResult result = await _controller.Update(
            4,
            new UpdateRerunCollectionRequest(
                "Nightly",
                CollectionType.RerunFirstRun,
                1,
                PlaybackOrder.Chronological,
                PlaybackOrder.Chronological),
            CancellationToken.None);

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

    [Test]
    public async Task Delete_Should_Return_204_On_Success()
    {
        _mediator.Send(Arg.Any<DeleteRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));

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

        result.ShouldBeOfType<NoContentResult>();
        await _mediator.Received(1).Send(
            Arg.Is<DeleteRerunCollection>(c => c.RerunCollectionId == 4),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Delete_Should_Return_404_On_NotFoundError()
    {
        _mediator.Send(Arg.Any<DeleteRerunCollection>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, Unit>(new ErsatzTV.Core.Errors.NotFoundError("missing")));

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

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    private static RerunCollectionViewModel MakeRerunFromSmart(int id, string name) =>
        new(
            id,
            name,
            CollectionType.SmartCollection,
            null,
            null,
            new SmartCollectionViewModel(20, "Smart", "query"),
            null,
            PlaybackOrder.Chronological,
            PlaybackOrder.Shuffle);

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

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