using System.Text.Json;
using ErsatzTV.Application.Channels;
using ErsatzTV.Core.Api.Channels;
using ErsatzTV.Core.Api.MediaItems;
using ErsatzTV.Core.Api.Settings;
using ErsatzTV.Core.Domain;
using ErsatzTV.Serialization;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using NUnit.Framework;
using Shouldly;

namespace ErsatzTV.Tests.Controllers;

/// <summary>
/// Guards that the generated OpenAPI document's property names for a set of DTOs exactly match the
/// JSON keys the runtime MVC serializer (Newtonsoft, via <see cref="CustomContractResolver" /> +
/// <see cref="StringEnumConverter" />) actually emits. The spec is generated from System.Text.Json
/// metadata, whose camelCase can drift from Newtonsoft's (e.g. "ffmpegProfileId" special case, or a
/// [JsonProperty] override). See issue #198 — a schema transformer now mirrors the runtime resolver,
/// and this test fails if that mirroring is ever removed or broken.
/// </summary>
[TestFixture]
public class OpenApiSerializerContractTests
{
    // Mirrors Startup.ConfigureServices -> AddNewtonsoftJson exactly.
    private static readonly JsonSerializerSettings RuntimeSettings = new()
    {
        NullValueHandling = NullValueHandling.Ignore,
        ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
        ContractResolver = new CustomContractResolver(),
        Converters = { new StringEnumConverter() }
    };

    private static IEnumerable<TestCaseData> Cases()
    {
        yield return new TestCaseData(FullyPopulatedChannelDetail(), "ChannelDetailResponseModel")
            .SetName("ChannelDetailResponseModel");
        yield return new TestCaseData(FullyPopulatedFFmpegSettings(), "FFmpegSettingsResponseModel")
            .SetName("FFmpegSettingsResponseModel");
        // WatermarkViewModel was dropped from the API surface by #126: schedule-item endpoints no longer
        // expose the polymorphic ProgramScheduleItemViewModel (which transitively pulled in WatermarkViewModel
        // and the other collection VMs) — they return the flat ScheduleItemResponseModel, embedding watermarks
        // as NamedIdResponseModel. ChannelDetailResponseModel above still exercises the "ffmpegProfileId"
        // special-case naming path of the schema transformer.
        yield return new TestCaseData(FullyPopulatedMediaItemInfo(), "MediaItemInfoResponseModel")
            .SetName("MediaItemInfoResponseModel");

        // The only DTO with a [JsonProperty] override (FFmpegProfile -> "ffmpegProfile") — covers
        // the attribute path of NewtonsoftSchemaNamingTransformer, which the cases above don't.
        yield return new TestCaseData(FullyPopulatedChannelSummary(), "ChannelResponseModel")
            .SetName("ChannelResponseModel");
    }

    [TestCaseSource(nameof(Cases))]
    public void Runtime_Serialized_Keys_Should_Match_OpenApi_Schema_Properties(object dto, string schemaName)
    {
        // Every member of dto is non-null, so NullValueHandling.Ignore drops nothing: the emitted
        // top-level keys are the complete runtime property set for this type.
        var serialized = JObject.Parse(JsonConvert.SerializeObject(dto, RuntimeSettings));
        List<string> runtimeKeys = serialized.Properties().Select(p => p.Name).OrderBy(n => n).ToList();

        using JsonDocument document = JsonDocument.Parse(File.ReadAllText(FindOpenApiDocument()));
        JsonElement properties = document.RootElement
            .GetProperty("components")
            .GetProperty("schemas")
            .GetProperty(schemaName)
            .GetProperty("properties");

        List<string> schemaKeys = properties.EnumerateObject().Select(p => p.Name).OrderBy(n => n).ToList();

        runtimeKeys.ShouldBe(
            schemaKeys,
            $"OpenAPI schema '{schemaName}' property names must match the runtime Newtonsoft JSON keys.");
    }

    private static ChannelDetailResponseModel FullyPopulatedChannelDetail() => new(
        1,
        "1",
        "Name",
        "Group",
        "Categories",
        1,
        1.0,
        new ChannelLogoResponseModel("path", "image/png"),
        default,
        "selector",
        "en",
        "Audio Title",
        default,
        default,
        1,
        TimeSpan.Zero,
        default,
        1,
        1,
        1,
        "en",
        default,
        default,
        "template",
        default,
        default,
        default,
        true,
        true,
        [1],
        new ChannelHealthResponseModel(ChannelHealthStatus.Healthy, [], 1, 0));

    private static FFmpegSettingsResponseModel FullyPopulatedFFmpegSettings() => new(
        "/usr/bin/ffmpeg",
        "/usr/bin/ffprobe",
        1,
        "en",
        true,
        true,
        true,
        true,
        1,
        1,
        1,
        1,
        1,
        default,
        "script");

    private static MediaItemInfoResponseModel FullyPopulatedMediaItemInfo() => new(
        1,
        "Title",
        "Movie",
        "Local",
        "Server",
        "Library",
        default,
        TimeSpan.Zero,
        "1:1",
        "16:9",
        "30/1",
        default,
        1.0,
        1,
        1,
        [],
        []);

    private static ChannelResponseModel FullyPopulatedChannelSummary() => new(
        1,
        "1",
        1.0,
        "Name",
        "Group",
        "Categories",
        "1080p H.264",
        "en",
        "TransportStream",
        true,
        true,
        2,
        "/iptv/logos/logo.png",
        Mapper.GetPreview(StreamingMode.HttpLiveStreamingSegmenter, "1", true, 2),
        ChannelOrigin.AutoTuned,
        new ChannelHealthResponseModel(ChannelHealthStatus.Healthy, [], 2, 0));

    private static string FindOpenApiDocument()
    {
        DirectoryInfo? directory = new(TestContext.CurrentContext.TestDirectory);
        while (directory is not null)
        {
            string candidate = Path.Combine(directory.FullName, "ErsatzTV", "wwwroot", "openapi", "v1.json");
            if (File.Exists(candidate))
            {
                return candidate;
            }

            directory = directory.Parent;
        }

        throw new FileNotFoundException("Could not find ErsatzTV/wwwroot/openapi/v1.json");
    }
}
