using System.Globalization;
using System.Reflection;
using ErsatzTV.Application.Artists;
using ErsatzTV.Application.MediaCards;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Movies;
using ErsatzTV.Application.Television;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Artists;
using ErsatzTV.Core.Api.MediaItems;
using ErsatzTV.Core.Api.Movies;
using ErsatzTV.Core.Api.Television;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Errors;
using LanguageExt;
using MediatR;
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 MediaDetailControllerTests
{
    private IMediator _mediator = null!;

    [SetUp]
    public void SetUp() => _mediator = Substitute.For<IMediator>();

    [Test]
    public void Controllers_Should_Expose_Idiomatic_Rest_Routes()
    {
        ShouldHaveActionRoute<MoviesController>(nameof(MoviesController.GetById), "GET", "/api/v1/movies/{id:int}");
        ShouldHaveActionRoute<ShowsController>(nameof(ShowsController.GetById), "GET", "/api/v1/shows/{id:int}");
        ShouldHaveActionRoute<SeasonsController>(nameof(SeasonsController.GetById), "GET", "/api/v1/seasons/{id:int}");
        ShouldHaveActionRoute<ArtistsController>(nameof(ArtistsController.GetById), "GET", "/api/v1/artists/{id:int}");
        ShouldHaveActionRoute<MediaItemsController>(
            nameof(MediaItemsController.GetInfo),
            "GET",
            "/api/v1/media-items/{id:int}/info");
    }

    [Test]
    public async Task Movie_Should_Return_200_With_Rooted_Artwork()
    {
        var vm = new MovieViewModel(
            "The Movie",
            "1999",
            "A plot",
            ["Drama"],
            ["tag"],
            ["Studio"],
            ["PG"],
            ["English"],
            [new ActorCardViewModel(7, "Actor", "Role", "actor.jpg", MediaItemState.Normal)],
            ["Director"],
            ["Writer"],
            "/media/movie.mkv",
            "/local/movie.mkv",
            MediaItemState.Normal)
        {
            Poster = "poster.jpg",
            FanArt = "https://example.com/fan.jpg"
        };
        _mediator.Send(Arg.Any<GetMovieById>(), Arg.Any<CancellationToken>())
            .Returns(Option<MovieViewModel>.Some(vm));

        var controller = new MoviesController(_mediator);
        IActionResult result = await controller.GetById(5, CancellationToken.None);

        var ok = result.ShouldBeOfType<OkObjectResult>();
        var body = ok.Value.ShouldBeOfType<MovieDetailResponseModel>();
        body.Id.ShouldBe(5);
        body.Title.ShouldBe("The Movie");
        body.Poster.ShouldBe("/artwork/posters/poster.jpg");
        body.FanArt.ShouldBe("https://example.com/fan.jpg");
        body.Actors.Single().Thumb.ShouldBe("/artwork/thumbnails/actor.jpg");
        body.State.ShouldBe(MediaItemState.Normal);
    }

    [Test]
    public async Task Movie_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetMovieById>(), Arg.Any<CancellationToken>())
            .Returns(Option<MovieViewModel>.None);

        var controller = new MoviesController(_mediator);
        IActionResult result = await controller.GetById(5, CancellationToken.None);

        result.ShouldBeOfType<NotFoundObjectResult>().StatusCode.ShouldBe(404);
    }

    [Test]
    public async Task Show_Should_Map_Languages_To_English_Names()
    {
        var vm = new TelevisionShowViewModel(
            3,
            2,
            MediaSourceKind.Local,
            "The Show",
            "2010",
            "Plot",
            "poster.jpg",
            "fan.jpg",
            ["Comedy"],
            ["tag"],
            ["Studio"],
            ["Network"],
            ["TV-14"],
            [new CultureInfo("en")],
            []);
        _mediator.Send(Arg.Any<GetTelevisionShowById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TelevisionShowViewModel>.Some(vm));

        var controller = new ShowsController(_mediator);
        IActionResult result = await controller.GetById(3, CancellationToken.None);

        var body = result.ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<ShowDetailResponseModel>();
        body.Id.ShouldBe(3);
        body.Poster.ShouldBe("/artwork/posters/poster.jpg");
        body.FanArt.ShouldBe("/artwork/fanart/fan.jpg");
        body.Languages.ShouldContain(new CultureInfo("en").EnglishName);
    }

    [Test]
    public async Task Show_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetTelevisionShowById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TelevisionShowViewModel>.None);

        var controller = new ShowsController(_mediator);
        (await controller.GetById(3, CancellationToken.None)).ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Season_Should_Return_200()
    {
        var vm = new TelevisionSeasonViewModel(4, 3, "Show", "2010", "Season 1", "s.jpg", "f.jpg");
        _mediator.Send(Arg.Any<GetTelevisionSeasonById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TelevisionSeasonViewModel>.Some(vm));

        var controller = new SeasonsController(_mediator);
        var body = (await controller.GetById(4, CancellationToken.None))
            .ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<SeasonDetailResponseModel>();
        body.ShowId.ShouldBe(3);
        body.Poster.ShouldBe("/artwork/posters/s.jpg");
    }

    [Test]
    public async Task Season_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetTelevisionSeasonById>(), Arg.Any<CancellationToken>())
            .Returns(Option<TelevisionSeasonViewModel>.None);

        var controller = new SeasonsController(_mediator);
        (await controller.GetById(4, CancellationToken.None)).ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task Artist_Should_Root_Thumbnail_And_FanArt()
    {
        var vm = new ArtistViewModel(
            "Artist",
            "Disambig",
            "Bio",
            "thumb.jpg",
            "fan.jpg",
            ["Rock"],
            ["Style"],
            ["Mood"],
            [new CultureInfo("en")]);
        _mediator.Send(Arg.Any<GetArtistById>(), Arg.Any<CancellationToken>())
            .Returns(Option<ArtistViewModel>.Some(vm));

        var controller = new ArtistsController(_mediator);
        var body = (await controller.GetById(6, CancellationToken.None))
            .ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<ArtistDetailResponseModel>();
        body.Id.ShouldBe(6);
        body.Thumbnail.ShouldBe("/artwork/thumbnails/thumb.jpg");
        body.FanArt.ShouldBe("/artwork/fanart/fan.jpg");
        body.Languages.ShouldContain(new CultureInfo("en").EnglishName);
    }

    [Test]
    public async Task Artist_Should_Return_404_When_Missing()
    {
        _mediator.Send(Arg.Any<GetArtistById>(), Arg.Any<CancellationToken>())
            .Returns(Option<ArtistViewModel>.None);

        var controller = new ArtistsController(_mediator);
        (await controller.GetById(6, CancellationToken.None)).ShouldBeOfType<NotFoundObjectResult>();
    }

    [Test]
    public async Task MediaItemInfo_Should_Return_200_With_Mapped_Streams()
    {
        var info = new MediaItemInfo(
            9,
            "Title",
            "Movie",
            "LocalLibrary",
            null,
            "Movies",
            MediaItemState.Normal,
            TimeSpan.FromMinutes(90),
            "1:1",
            "16:9",
            "24/1",
            VideoScanKind.Progressive,
            null,
            1920,
            1080,
            [new MediaItemInfoStream(0, MediaStreamKind.Video, "v", "h264", "high", "eng", null, true, null, null, "yuv420p", null, null, null, null, 8, null)],
            [new MediaItemInfoChapter("Chapter 1", TimeSpan.Zero, TimeSpan.FromMinutes(10))]);
        _mediator.Send(Arg.Any<GetMediaItemInfo>(), Arg.Any<CancellationToken>())
            .Returns(Either<BaseError, MediaItemInfo>.Right(info));

        var controller = new MediaItemsController(_mediator);
        var body = (await controller.GetInfo(9, CancellationToken.None))
            .ShouldBeOfType<OkObjectResult>().Value.ShouldBeOfType<MediaItemInfoResponseModel>();
        body.Id.ShouldBe(9);
        body.Streams.Single().Codec.ShouldBe("h264");
        body.Chapters.Single().Title.ShouldBe("Chapter 1");
    }

    [Test]
    public async Task MediaItemInfo_Should_Return_404_When_Not_Located()
    {
        _mediator.Send(Arg.Any<GetMediaItemInfo>(), Arg.Any<CancellationToken>())
            .Returns(Either<BaseError, MediaItemInfo>.Left(new UnableToLocateMediaItem()));

        var controller = new MediaItemsController(_mediator);
        (await controller.GetInfo(9, CancellationToken.None))
            .ShouldBeOfType<NotFoundObjectResult>().StatusCode.ShouldBe(404);
    }

    [Test]
    public async Task MediaItemInfo_Should_Return_422_On_Other_Error()
    {
        _mediator.Send(Arg.Any<GetMediaItemInfo>(), Arg.Any<CancellationToken>())
            .Returns(Either<BaseError, MediaItemInfo>.Left(BaseError.New("boom")));

        var controller = new MediaItemsController(_mediator);
        (await controller.GetInfo(9, CancellationToken.None))
            .ShouldBeOfType<UnprocessableEntityObjectResult>().StatusCode.ShouldBe(422);
    }

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

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