using ErsatzTV.Application.Configuration;
using ErsatzTV.Application.FFmpegProfiles;
using ErsatzTV.Application.HDHR;
using ErsatzTV.Controllers.Api;
using ErsatzTV.Controllers.Api.Requests;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Settings;
using ErsatzTV.FFmpeg.OutputFormat;
using LanguageExt;
using MediatR;
using Microsoft.AspNetCore.Mvc;
using NSubstitute;
using NUnit.Framework;
using Serilog.Events;
using Shouldly;
using static LanguageExt.Prelude;
using ApiXmltvBlockBehavior = ErsatzTV.Core.Api.Settings.XmltvBlockBehavior;
using ApiXmltvTimeZone = ErsatzTV.Core.Api.Settings.XmltvTimeZone;
using Unit = LanguageExt.Unit;
using VmXmltvBlockBehavior = ErsatzTV.Application.Configuration.XmltvBlockBehavior;
using VmXmltvTimeZone = ErsatzTV.Application.Configuration.XmltvTimeZone;

namespace ErsatzTV.Tests.Controllers;

[TestFixture]
public class SettingsControllerTests
{
    private SettingsController _controller = null!;
    private IMediator _mediator = null!;

    [SetUp]
    public void SetUp()
    {
        _mediator = Substitute.For<IMediator>();
        _controller = new SettingsController(_mediator);
    }

    [Test]
    public async Task GetFfmpeg_Should_Map_Vm_To_Response_Model()
    {
        var vm = new FFmpegSettingsViewModel
        {
            FFmpegPath = "/usr/bin/ffmpeg",
            FFprobePath = "/usr/bin/ffprobe",
            DefaultFFmpegProfileId = 1,
            PreferredAudioLanguageCode = "eng",
            UseEmbeddedSubtitles = true,
            ExtractEmbeddedSubtitles = false,
            ProbeForInterlacedFrames = false,
            SaveReports = false,
            GlobalWatermarkId = 5,
            GlobalFallbackFillerId = null,
            HlsSegmenterIdleTimeout = 60,
            WorkAheadSegmenterLimit = 1,
            InitialSegmentCount = 1,
            HlsDirectOutputFormat = OutputFormatKind.MpegTs,
            DefaultMpegTsScript = "Default"
        };
        _mediator.Send(Arg.Any<GetFFmpegSettings>(), Arg.Any<CancellationToken>()).Returns(vm);

        FFmpegSettingsResponseModel result = await _controller.GetFfmpeg(CancellationToken.None);

        result.FFmpegPath.ShouldBe("/usr/bin/ffmpeg");
        result.GlobalWatermarkId.ShouldBe(5);
        result.GlobalFallbackFillerId.ShouldBeNull();
        result.HlsDirectOutputFormat.ShouldBe(OutputFormatKind.MpegTs);
    }

    [Test]
    public async Task UpdateFfmpeg_Should_Map_Request_To_Command_And_Return_Refreshed_Settings()
    {
        _mediator.Send(Arg.Any<UpdateFFmpegSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        var refreshed = new FFmpegSettingsViewModel
        {
            FFmpegPath = "/usr/bin/ffmpeg",
            FFprobePath = "/usr/bin/ffprobe",
            PreferredAudioLanguageCode = "eng",
            HlsDirectOutputFormat = OutputFormatKind.Hls,
            DefaultMpegTsScript = "Default"
        };
        _mediator.Send(Arg.Any<GetFFmpegSettings>(), Arg.Any<CancellationToken>()).Returns(refreshed);

        IActionResult result = await _controller.UpdateFfmpeg(MakeFfmpegRequest(), CancellationToken.None);

        var ok = result.ShouldBeOfType<OkObjectResult>();
        ok.Value.ShouldBeOfType<FFmpegSettingsResponseModel>().HlsDirectOutputFormat.ShouldBe(OutputFormatKind.Hls);
        await _mediator.Received(1).Send(
            Arg.Is<UpdateFFmpegSettings>(
                c => c.Settings.FFmpegPath == "/usr/bin/ffmpeg" &&
                    c.Settings.FFprobePath == "/usr/bin/ffprobe" &&
                    c.Settings.DefaultFFmpegProfileId == 1 &&
                    c.Settings.PreferredAudioLanguageCode == "eng" &&
                    c.Settings.UseEmbeddedSubtitles &&
                    !c.Settings.ExtractEmbeddedSubtitles &&
                    !c.Settings.ProbeForInterlacedFrames &&
                    !c.Settings.SaveReports &&
                    c.Settings.GlobalWatermarkId == null &&
                    c.Settings.GlobalFallbackFillerId == null &&
                    c.Settings.HlsSegmenterIdleTimeout == 60 &&
                    c.Settings.WorkAheadSegmenterLimit == 1 &&
                    c.Settings.InitialSegmentCount == 1 &&
                    c.Settings.HlsDirectOutputFormat == OutputFormatKind.MpegTs &&
                    c.Settings.DefaultMpegTsScript == "Default"),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task UpdateFfmpeg_Should_Flow_NonNull_Watermark_And_Filler_Ids_Through()
    {
        _mediator.Send(Arg.Any<UpdateFFmpegSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetFFmpegSettings>(), Arg.Any<CancellationToken>())
            .Returns(new FFmpegSettingsViewModel { FFmpegPath = "/usr/bin/ffmpeg", FFprobePath = "/usr/bin/ffprobe" });

        UpdateFFmpegSettingsRequest request = MakeFfmpegRequest() with { GlobalWatermarkId = 7, GlobalFallbackFillerId = 9 };

        IActionResult result = await _controller.UpdateFfmpeg(request, CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>();
        await _mediator.Received(1).Send(
            Arg.Is<UpdateFFmpegSettings>(
                c => c.Settings.GlobalWatermarkId == 7 && c.Settings.GlobalFallbackFillerId == 9),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task UpdateFfmpeg_Should_Return_422_On_Validation_Error()
    {
        _mediator.Send(Arg.Any<UpdateFFmpegSettings>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, Unit>(BaseError.New("ffmpeg path does not exist")));

        IActionResult result = await _controller.UpdateFfmpeg(MakeFfmpegRequest(), CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task GetPlayout_Should_Map_Vm_To_Response_Model()
    {
        var vm = new PlayoutSettingsViewModel
        {
            DaysToBuild = 3,
            SkipMissingItems = true,
            ScriptedScheduleTimeoutSeconds = 45
        };
        _mediator.Send(Arg.Any<GetPlayoutSettings>(), Arg.Any<CancellationToken>()).Returns(vm);

        PlayoutSettingsResponseModel result = await _controller.GetPlayout(CancellationToken.None);

        result.ShouldBe(new PlayoutSettingsResponseModel(3, true, 45));
    }

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

        IActionResult result = await _controller.UpdatePlayout(
            new UpdatePlayoutSettingsRequest(0, false, 30),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task UpdatePlayout_Should_Return_Refreshed_Settings_On_Success()
    {
        _mediator.Send(Arg.Any<UpdatePlayoutSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetPlayoutSettings>(), Arg.Any<CancellationToken>())
            .Returns(new PlayoutSettingsViewModel { DaysToBuild = 5, SkipMissingItems = false, ScriptedScheduleTimeoutSeconds = 30 });

        IActionResult result = await _controller.UpdatePlayout(
            new UpdatePlayoutSettingsRequest(5, false, 30),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(new PlayoutSettingsResponseModel(5, false, 30));
        await _mediator.Received(1).Send(
            Arg.Is<UpdatePlayoutSettings>(
                c => c.PlayoutSettings.DaysToBuild == 5 &&
                    c.PlayoutSettings.SkipMissingItems == false &&
                    c.PlayoutSettings.ScriptedScheduleTimeoutSeconds == 30),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetXmltv_Should_Map_Vm_Enums_To_Api_Enums()
    {
        _mediator.Send(Arg.Any<GetXmltvSettings>(), Arg.Any<CancellationToken>())
            .Returns(
                new XmltvSettingsViewModel
                {
                    DaysToBuild = 2,
                    TimeZone = VmXmltvTimeZone.Utc,
                    BlockBehavior = VmXmltvBlockBehavior.UseActualTimes
                });

        XmltvSettingsResponseModel result = await _controller.GetXmltv(CancellationToken.None);

        result.ShouldBe(
            new XmltvSettingsResponseModel(
                2,
                ApiXmltvTimeZone.Utc,
                ApiXmltvBlockBehavior.UseActualTimes));
    }

    [Test]
    public async Task UpdateXmltv_Should_Map_Api_Enums_To_Vm_Enums()
    {
        _mediator.Send(Arg.Any<UpdateXmltvSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetXmltvSettings>(), Arg.Any<CancellationToken>())
            .Returns(new XmltvSettingsViewModel { DaysToBuild = 2, TimeZone = VmXmltvTimeZone.Local, BlockBehavior = VmXmltvBlockBehavior.SplitTimeEvenly });

        IActionResult result = await _controller.UpdateXmltv(
            new UpdateXmltvSettingsRequest(2, ApiXmltvTimeZone.Local, ApiXmltvBlockBehavior.SplitTimeEvenly),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>();
        await _mediator.Received(1).Send(
            Arg.Is<UpdateXmltvSettings>(
                c => c.XmltvSettings.TimeZone == VmXmltvTimeZone.Local &&
                    c.XmltvSettings.BlockBehavior == VmXmltvBlockBehavior.SplitTimeEvenly),
            Arg.Any<CancellationToken>());
    }

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

        IActionResult result = await _controller.UpdateXmltv(
            new UpdateXmltvSettingsRequest(
                2,
                ErsatzTV.Core.Api.Settings.XmltvTimeZone.Local,
                ErsatzTV.Core.Api.Settings.XmltvBlockBehavior.SplitTimeEvenly),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task GetIptv_Should_Map_Vm_To_Response_Model()
    {
        _mediator.Send(Arg.Any<GetIptvSettings>(), Arg.Any<CancellationToken>())
            .Returns(new IptvSettingsViewModel { BaseUrl = "https://tv.example.com/etv" });

        IptvSettingsResponseModel result = await _controller.GetIptv(CancellationToken.None);

        result.ShouldBe(new IptvSettingsResponseModel("https://tv.example.com/etv"));
    }

    [Test]
    public async Task UpdateIptv_Should_Map_Request_To_Command_And_Return_Refreshed_Settings()
    {
        _mediator.Send(Arg.Any<UpdateIptvSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetIptvSettings>(), Arg.Any<CancellationToken>())
            .Returns(new IptvSettingsViewModel { BaseUrl = "https://public.example.com" });

        IActionResult result = await _controller.UpdateIptv(
            new UpdateIptvSettingsRequest("https://public.example.com"),
            CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(new IptvSettingsResponseModel("https://public.example.com"));
        await _mediator.Received(1).Send(
            Arg.Is<UpdateIptvSettings>(c => c.IptvSettings.BaseUrl == "https://public.example.com"),
            Arg.Any<CancellationToken>());
    }

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

        IActionResult result = await _controller.UpdateIptv(
            new UpdateIptvSettingsRequest("not a url"),
            CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task GetScanner_Should_Return_Library_Refresh_Interval()
    {
        _mediator.Send(Arg.Any<GetLibraryRefreshInterval>(), Arg.Any<CancellationToken>()).Returns(6);

        ScannerSettingsResponseModel result = await _controller.GetScanner(CancellationToken.None);

        result.ShouldBe(new ScannerSettingsResponseModel(6));
    }

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

        IActionResult result =
            await _controller.UpdateScanner(new UpdateScannerSettingsRequest(-1), CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

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

        IActionResult result =
            await _controller.UpdateScanner(new UpdateScannerSettingsRequest(12), CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(new ScannerSettingsResponseModel(12));
        await _mediator.Received(1).Send(
            Arg.Is<UpdateLibraryRefreshInterval>(c => c.LibraryRefreshInterval == 12),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetLogging_Should_Map_Vm_To_Response_Model()
    {
        var vm = new LoggingSettingsViewModel
        {
            DefaultMinimumLogLevel = LogEventLevel.Information,
            ScanningMinimumLogLevel = LogEventLevel.Debug,
            SchedulingMinimumLogLevel = LogEventLevel.Warning,
            SearchingMinimumLogLevel = LogEventLevel.Error,
            StreamingMinimumLogLevel = LogEventLevel.Verbose,
            HttpMinimumLogLevel = LogEventLevel.Fatal
        };
        _mediator.Send(Arg.Any<GetLoggingSettings>(), Arg.Any<CancellationToken>()).Returns(vm);

        LoggingSettingsResponseModel result = await _controller.GetLogging(CancellationToken.None);

        result.DefaultMinimumLogLevel.ShouldBe(LogEventLevel.Information);
        result.ScanningMinimumLogLevel.ShouldBe(LogEventLevel.Debug);
        result.SchedulingMinimumLogLevel.ShouldBe(LogEventLevel.Warning);
        result.SearchingMinimumLogLevel.ShouldBe(LogEventLevel.Error);
        result.StreamingMinimumLogLevel.ShouldBe(LogEventLevel.Verbose);
        result.HttpMinimumLogLevel.ShouldBe(LogEventLevel.Fatal);
    }

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

        IActionResult result = await _controller.UpdateLogging(MakeLoggingRequest(), CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task UpdateLogging_Should_Map_Request_To_Command_And_Return_Refreshed_Settings()
    {
        _mediator.Send(Arg.Any<UpdateLoggingSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        var refreshed = new LoggingSettingsViewModel
        {
            DefaultMinimumLogLevel = LogEventLevel.Debug,
            ScanningMinimumLogLevel = LogEventLevel.Debug,
            SchedulingMinimumLogLevel = LogEventLevel.Debug,
            SearchingMinimumLogLevel = LogEventLevel.Debug,
            StreamingMinimumLogLevel = LogEventLevel.Debug,
            HttpMinimumLogLevel = LogEventLevel.Debug
        };
        _mediator.Send(Arg.Any<GetLoggingSettings>(), Arg.Any<CancellationToken>()).Returns(refreshed);

        var request = new UpdateLoggingSettingsRequest(
            LogEventLevel.Warning,
            LogEventLevel.Error,
            LogEventLevel.Fatal,
            LogEventLevel.Verbose,
            LogEventLevel.Debug,
            LogEventLevel.Information);

        IActionResult result = await _controller.UpdateLogging(request, CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>();
        await _mediator.Received(1).Send(
            Arg.Is<UpdateLoggingSettings>(
                c => c.LoggingSettings.DefaultMinimumLogLevel == LogEventLevel.Warning &&
                    c.LoggingSettings.ScanningMinimumLogLevel == LogEventLevel.Error &&
                    c.LoggingSettings.SchedulingMinimumLogLevel == LogEventLevel.Fatal &&
                    c.LoggingSettings.SearchingMinimumLogLevel == LogEventLevel.Verbose &&
                    c.LoggingSettings.StreamingMinimumLogLevel == LogEventLevel.Debug &&
                    c.LoggingSettings.HttpMinimumLogLevel == LogEventLevel.Information),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task GetUi_Should_Map_Vm_To_Response_Model()
    {
        _mediator.Send(Arg.Any<GetUiSettings>(), Arg.Any<CancellationToken>())
            .Returns(new UiSettingsViewModel { IsDarkMode = true, Language = "en" });

        UiSettingsResponseModel result = await _controller.GetUi(CancellationToken.None);

        result.ShouldBe(new UiSettingsResponseModel(true, "en"));
    }

    [Test]
    public async Task UpdateUi_Should_Return_Refreshed_Settings_On_Success()
    {
        _mediator.Send(Arg.Any<UpdateUiSettings>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetUiSettings>(), Arg.Any<CancellationToken>())
            .Returns(new UiSettingsViewModel { IsDarkMode = false, Language = "fr" });

        IActionResult result =
            await _controller.UpdateUi(new UpdateUiSettingsRequest(false, "fr"), CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(new UiSettingsResponseModel(false, "fr"));
        await _mediator.Received(1).Send(
            Arg.Is<UpdateUiSettings>(c => c.UiSettings.IsDarkMode == false && c.UiSettings.Language == "fr"),
            Arg.Any<CancellationToken>());
    }

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

        IActionResult result =
            await _controller.UpdateUi(new UpdateUiSettingsRequest(false, "fr"), CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    [Test]
    public async Task GetHdhr_Should_Combine_Tuner_Count_And_Uuid()
    {
        var uuid = Guid.NewGuid();
        _mediator.Send(Arg.Any<GetHDHRTunerCount>(), Arg.Any<CancellationToken>()).Returns(3);
        _mediator.Send(Arg.Any<GetHDHRUUID>(), Arg.Any<CancellationToken>()).Returns(uuid);

        HdhrSettingsResponseModel result = await _controller.GetHdhr(CancellationToken.None);

        result.ShouldBe(new HdhrSettingsResponseModel(3, uuid));
    }

    [Test]
    public async Task UpdateHdhr_Should_Return_Refreshed_Settings_On_Success()
    {
        var uuid = Guid.NewGuid();
        _mediator.Send(Arg.Any<UpdateHDHRTunerCount>(), Arg.Any<CancellationToken>())
            .Returns(Right<BaseError, Unit>(Unit.Default));
        _mediator.Send(Arg.Any<GetHDHRTunerCount>(), Arg.Any<CancellationToken>()).Returns(4);
        _mediator.Send(Arg.Any<GetHDHRUUID>(), Arg.Any<CancellationToken>()).Returns(uuid);

        IActionResult result = await _controller.UpdateHdhr(new UpdateHdhrSettingsRequest(4), CancellationToken.None);

        result.ShouldBeOfType<OkObjectResult>().Value.ShouldBe(new HdhrSettingsResponseModel(4, uuid));
        await _mediator.Received(1).Send(
            Arg.Is<UpdateHDHRTunerCount>(c => c.TunerCount == 4),
            Arg.Any<CancellationToken>());
    }

    [Test]
    public async Task UpdateHdhr_Should_Return_422_On_Validation_Error()
    {
        _mediator.Send(Arg.Any<UpdateHDHRTunerCount>(), Arg.Any<CancellationToken>())
            .Returns(Left<BaseError, Unit>(BaseError.New("Tuner count must be greater than zero")));

        IActionResult result = await _controller.UpdateHdhr(new UpdateHdhrSettingsRequest(0), CancellationToken.None);

        result.ShouldBeOfType<UnprocessableEntityObjectResult>();
    }

    private static UpdateFFmpegSettingsRequest MakeFfmpegRequest() =>
        new(
            "/usr/bin/ffmpeg",
            "/usr/bin/ffprobe",
            1,
            "eng",
            true,
            false,
            false,
            false,
            null,
            null,
            60,
            1,
            1,
            OutputFormatKind.MpegTs,
            "Default");

    private static UpdateLoggingSettingsRequest MakeLoggingRequest() =>
        new(
            LogEventLevel.Information,
            LogEventLevel.Information,
            LogEventLevel.Information,
            LogEventLevel.Information,
            LogEventLevel.Information,
            LogEventLevel.Information);
}
