using System.Reflection;
using ErsatzTV.Application.Scheduling;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Scheduling;
using ErsatzTV.Core.Errors;
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;

namespace ErsatzTV.Tests.Controllers;

[TestFixture]
public class DecoTemplateControllerTests
{
    private DecoTemplateController _controller = null!;
    private IMediator _mediator = null!;

    [SetUp]
    public void SetUp()
    {
        _mediator = Substitute.For<IMediator>();
        _controller = new DecoTemplateController(_mediator)
        {
            // Provide a real HttpContext so the ETag/If-Match concurrency headers (#253) can be
            // read from Request and written to Response.
            ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() }
        };
    }

    [Test]
    public void Controller_Should_Expose_Idiomatic_Rest_Routes()
    {
        ShouldHaveActionRoute(nameof(DecoTemplateController.GetGroups), "GET", "/api/v1/deco-templates/groups");
        ShouldHaveActionRoute(nameof(DecoTemplateController.CreateGroup), "POST", "/api/v1/deco-templates/groups");
        ShouldHaveActionRoute(
            nameof(DecoTemplateController.DeleteGroup),
            "DELETE",
            "/api/v1/deco-templates/groups/{id:int}");
        ShouldHaveActionRoute(nameof(DecoTemplateController.GetAll), "GET", "/api/v1/deco-templates");
        ShouldHaveActionRoute(nameof(DecoTemplateController.GetById), "GET", "/api/v1/deco-templates/{id:int}");
        ShouldHaveActionRoute(nameof(DecoTemplateController.Create), "POST", "/api/v1/deco-templates");
        ShouldHaveActionRoute(nameof(DecoTemplateController.Delete), "DELETE", "/api/v1/deco-templates/{id:int}");
        ShouldHaveActionRoute(nameof(DecoTemplateController.GetItems), "GET", "/api/v1/deco-templates/{id:int}/items");
        ShouldHaveActionRoute(nameof(DecoTemplateController.Replace), "PUT", "/api/v1/deco-templates/{id:int}");
    }

    [Test]
    public async Task CreateGroup_Should_Return_201_With_Location_And_Body()
    {
        _mediator.Send(Arg.Any<CreateDecoTemplateGroup>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, DecoTemplateGroupViewModel>(new DecoTemplateGroupViewModel(5, "Weekday", 0)));

        IActionResult result = await _controller.CreateGroup(
            new CreateDecoTemplateGroupRequest("Weekday"),
            CancellationToken.None);

        var created = result.ShouldBeOfType<CreatedResult>();
        created.Location.ShouldBe("/api/v1/deco-templates/groups/5");
        created.Value.ShouldBeOfType<DecoTemplateGroupResponseModel>().Name.ShouldBe("Weekday");
        await _mediator.Received(1).Send(
            Arg.Is<CreateDecoTemplateGroup>(c => c.Name == "Weekday"),
            Arg.Any<CancellationToken>());
    }

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

        IActionResult result = await _controller.CreateGroup(
            new CreateDecoTemplateGroupRequest(string.Empty),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task DeleteGroup_Should_Return_204_When_Exists()
    {
        _mediator.Send(Arg.Any<GetAllDecoTemplateGroups>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateGroupViewModel> { new(3, "Weekday", 0) });
        _mediator.Send(Arg.Any<DeleteDecoTemplateGroup>(), Arg.Any<CancellationToken>())
            .Returns(Option<BaseError>.None);

        IActionResult result = await _controller.DeleteGroup(3, CancellationToken.None);

        result.ShouldBeOfType<NoContentResult>();
        await _mediator.Received(1).Send(
            Arg.Is<DeleteDecoTemplateGroup>(c => c.DecoTemplateGroupId == 3),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task DeleteGroup_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetAllDecoTemplateGroups>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateGroupViewModel>());

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

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

    [Test]
    public async Task GetAll_Should_Flatten_DecoTemplates_Across_Groups()
    {
        _mediator.Send(Arg.Any<GetAllDecoTemplateGroups>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateGroupViewModel> { new(2, "Weekday", 1) });
        _mediator.Send(Arg.Any<GetDecoTemplatesByDecoTemplateGroupId>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateViewModel> { MakeDecoTemplate(1, 2, "Morning") });

        List<DecoTemplateResponseModel> result = await _controller.GetAll(CancellationToken.None);

        result.Count.ShouldBe(1);
        result[0].Id.ShouldBe(1);
        result[0].DecoTemplateGroupId.ShouldBe(2);
        result[0].Name.ShouldBe("Morning");
        await _mediator.Received(1).Send(
            Arg.Is<GetDecoTemplatesByDecoTemplateGroupId>(c => c.DecoTemplateGroupId == 2),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetById_Should_Return_200_For_Some()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 2, "Morning")));

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

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

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

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

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Create_Should_Return_201_And_Map_Request()
    {
        _mediator.Send(Arg.Any<CreateDecoTemplate>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, DecoTemplateViewModel>(MakeDecoTemplate(8, 2, "Morning")));

        IActionResult result = await _controller.Create(
            new CreateDecoTemplateRequest(2, "Morning"),
            CancellationToken.None);

        var created = result.ShouldBeOfType<CreatedResult>();
        created.Location.ShouldBe("/api/v1/deco-templates/8");
        await _mediator.Received(1).Send(
            Arg.Is<CreateDecoTemplate>(c => c.DecoTemplateGroupId == 2 && c.Name == "Morning"),
            Arg.Any<CancellationToken>());
    }

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

        IActionResult result = await _controller.Create(
            new CreateDecoTemplateRequest(2, "Morning"),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task Delete_Should_Return_204_When_Exists()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 2, "Morning")));
        _mediator.Send(Arg.Any<DeleteDecoTemplate>(), Arg.Any<CancellationToken>())
            .Returns(Option<BaseError>.None);

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

        result.ShouldBeOfType<NoContentResult>();
    }

    [Test]
    public async Task Delete_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.None);

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

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

    [Test]
    public async Task GetItems_Should_Return_200_Ordered_By_StartTime()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 2, "Morning")));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateItemViewModel>
            {
                MakeItem(20, "News Bumper", TimeSpan.FromHours(12), TimeSpan.FromHours(13)),
                MakeItem(10, "Morning Bumper", TimeSpan.FromHours(6), TimeSpan.FromHours(7))
            });

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

        var items = result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<List<DecoTemplateItemResponseModel>>();
        items.Count.ShouldBe(2);
        items[0].DecoId.ShouldBe(10);
        items[0].StartTime.ShouldBe(TimeSpan.FromHours(6));
        items[0].EndTime.ShouldBe(TimeSpan.FromHours(7));
        items[1].DecoId.ShouldBe(20);
    }

    [Test]
    public async Task GetItems_Should_Return_End_Of_Day_As_Midnight()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 2, "Morning")));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateItemViewModel>
            {
                MakeItem(10, "Overnight Bumper", TimeSpan.FromHours(22), TimeSpan.Zero)
            });

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

        var items = result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<List<DecoTemplateItemResponseModel>>();
        items[0].StartTime.ShouldBe(TimeSpan.FromHours(22));
        items[0].EndTime.ShouldBe(TimeSpan.Zero);
    }

    [Test]
    public async Task GetItems_Should_Return_404_When_DecoTemplate_Missing()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.None);

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

        result.ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Replace_Should_Return_200_With_Refreshed_DecoTemplate_And_Map_Items()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 7, "Morning")));
        _mediator.Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, List<DecoTemplateItemViewModel>>(
                [MakeItem(10, "Morning Bumper", TimeSpan.FromHours(6), TimeSpan.FromHours(7))]));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns([MakeItem(10, "Morning Bumper", TimeSpan.FromHours(6), TimeSpan.FromHours(7))]);

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest(
                "Morning",
                [new DecoTemplateItemRequest(10, TimeSpan.FromHours(6), TimeSpan.FromHours(7))]),
            CancellationToken.None);

        var body = result.ShouldBeOfType<OkObjectResult>().Value
            .ShouldBeOfType<DecoTemplateWithItemsResponseModel>();
        body.Id.ShouldBe(4);
        body.Items.Count.ShouldBe(1);
        await _mediator.Received(1).Send(
            Arg.Is<ReplaceDecoTemplateItems>(c =>
                c.DecoTemplateId == 4 &&
                c.DecoTemplateGroupId == 7 &&
                c.Name == "Morning" &&
                c.Items.Count == 1 &&
                c.Items[0].DecoId == 10 &&
                c.Items[0].StartTime == TimeSpan.FromHours(6) &&
                c.Items[0].EndTime == TimeSpan.FromHours(7)),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Replace_Should_Return_404_When_DecoTemplate_Missing()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.None);

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

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

    [Test]
    public async Task Replace_Should_Return_422_On_Validation_Error()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 7, "Morning")));
        _mediator.Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, List<DecoTemplateItemViewModel>>(BaseError.New("overlap")));

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task GetItems_Should_Set_ETag_From_DecoTemplate_Version()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 2, "Morning", version: 9)));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateItemViewModel>());

        await _controller.GetItems(4, CancellationToken.None);

        _controller.Response.Headers.ETag.ToString().ShouldBe("\"9\"");
    }

    [Test]
    public async Task Replace_Should_Return_400_On_Malformed_If_Match()
    {
        _controller.Request.Headers.IfMatch = "not-an-etag";

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

        result.ShouldBeOfType<BadRequestObjectResult>();
        await _mediator.DidNotReceive().Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>());
        await _mediator.DidNotReceive().Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Replace_Should_Thread_If_Match_Version_Into_Command()
    {
        _controller.Request.Headers.IfMatch = "\"3\"";
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 7, "Morning", version: 4)));
        _mediator.Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, List<DecoTemplateItemViewModel>>([]));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateItemViewModel>());

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>();
        // On success the response carries the refreshed deco template's ETag.
        _controller.Response.Headers.ETag.ToString().ShouldBe("\"4\"");
        await _mediator.Received(1).Send(
            Arg.Is<ReplaceDecoTemplateItems>(c => c.ExpectedVersions == Option<Seq<int>>.Some(new[] { 3 }.ToSeq())),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Replace_Without_If_Match_Should_Force_Write()
    {
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 7, "Morning")));
        _mediator.Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, List<DecoTemplateItemViewModel>>([]));
        _mediator.Send(Arg.Any<GetDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(new List<DecoTemplateItemViewModel>());

        await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

        await _mediator.Received(1).Send(
            Arg.Is<ReplaceDecoTemplateItems>(c => c.ExpectedVersions == Option<Seq<int>>.None),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task Replace_Should_Return_412_On_Precondition_Failed()
    {
        _controller.Request.Headers.IfMatch = "\"2\"";
        _mediator.Send(Arg.Any<GetDecoTemplateById>(), Arg.Any<CancellationToken>())
            .Returns(Option<DecoTemplateViewModel>.Some(MakeDecoTemplate(4, 7, "Morning", version: 5)));
        _mediator.Send(Arg.Any<ReplaceDecoTemplateItems>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, List<DecoTemplateItemViewModel>>(new PreconditionFailedError("stale")));

        IActionResult result = await _controller.Replace(
            4,
            new ReplaceDecoTemplateRequest("Morning", []),
            CancellationToken.None);

        var objectResult = result.ShouldBeOfType<ObjectResult>();
        objectResult.StatusCode.ShouldBe(StatusCodes.Status412PreconditionFailed);
    }

    private static DecoTemplateViewModel MakeDecoTemplate(int id, int groupId, string name, int version = 1) =>
        new(id, groupId, "Group", name, version);

    private static DecoTemplateItemViewModel MakeItem(int decoId, string decoName, TimeSpan startTime, TimeSpan endTime)
    {
        DateTime start = DateTime.Today.Add(startTime);
        DateTime end = DateTime.Today.Add(endTime);
        if (start > end)
        {
            end = end.AddDays(1);
        }

        return new DecoTemplateItemViewModel(decoId, decoName, start, end);
    }

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

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