using Bunit;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared;
using ZB.MOM.WW.ScadaBridge.CentralUI.Services;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Design;

/// <summary>
/// M9-T31 (+#261 follow-up): the raw-JSON escape hatch in
/// <see cref="ParameterValueForm"/> (the fallback for an unresolved <c>$ref</c> /
/// unknown-type node) is a Monaco <c>json</c> editor wired with THAT FIELD's own
/// sub-schema — not the whole root object schema — so each field's editor
/// validates/completes against just its own portion of the type. These tests
/// assert at the C#/interop boundary that the correct per-field fragment reaches
/// each editor's <c>JsonSchema</c> parameter (and that a sibling field's resolved
/// shape never leaks in), using <c>FindComponents</c> when 2+ escape-hatch fields
/// are present. The JS hover/completion itself is Monaco-built-in and not
/// unit-testable here.
/// </summary>
public class ParameterValueFormMonacoSchemaTests : BunitContext
{
    private readonly ISchemaLibraryQueryService _library = Substitute.For<ISchemaLibraryQueryService>();

    public ParameterValueFormMonacoSchemaTests()
    {
        // The escape-hatch surface renders a MonacoEditor whose JS interop is not
        // exercised here — Loose mode lets the unconfigured createEditor call no-op
        // so the render completes and we can assert on the editor's parameters.
        JSInterop.Mode = JSRuntimeMode.Loose;
        _library.GetSchemaMapAsync(Arg.Any<CancellationToken>())
            .Returns(new Dictionary<string, string>());
        Services.AddSingleton(_library);
    }

    [Fact]
    public void EscapeHatch_RendersMonacoJsonEditor_NotPlainTextarea()
    {
        // A nested field whose $ref cannot be resolved drops to the raw-JSON
        // escape hatch. That surface must now be a Monaco json editor.
        const string schema = """
            {
              "type": "object",
              "properties": {
                "payload": { "$ref": "lib:Missing" }
              }
            }
            """;

        var cut = Render<ParameterValueForm>(p => p
            .Add(x => x.ParameterDefinitions, schema)
            .Add(x => x.Values, new Dictionary<string, object?>()));

        var editor = cut.FindComponent<MonacoEditor>();
        Assert.Equal("json", editor.Instance.Language);
        // No plain <textarea> fallback remains for the escape hatch.
        Assert.Empty(cut.FindAll("textarea"));
    }

    [Fact]
    public void EscapeHatch_MonacoEditor_ReceivesFieldSubSchema_NotRootSchema()
    {
        // The library entry "Address" resolves to an object with street/city.
        const string libSchema = """
            {
              "type": "object",
              "properties": {
                "street": { "type": "string" },
                "city": { "type": "string" }
              }
            }
            """;
        _library.GetSchemaMapAsync(Arg.Any<CancellationToken>())
            .Returns(new Dictionary<string, string> { ["Address"] = libSchema });

        // Root has a RESOLVABLE $ref field (shipTo → Address) plus an
        // UNRESOLVABLE one (payload) that forces the escape-hatch Monaco editor.
        // The payload editor must receive PAYLOAD's own sub-schema — NOT the root
        // object schema, which would leak the sibling shipTo's street/city shape.
        const string schema = """
            {
              "type": "object",
              "properties": {
                "shipTo": { "$ref": "lib:Address" },
                "payload": { "$ref": "lib:Missing" }
              }
            }
            """;

        var cut = Render<ParameterValueForm>(p => p
            .Add(x => x.ParameterDefinitions, schema)
            .Add(x => x.Values, new Dictionary<string, object?>()));

        var editor = cut.FindComponent<MonacoEditor>();
        var jsonSchema = editor.Instance.JsonSchema;

        Assert.False(string.IsNullOrWhiteSpace(jsonSchema));
        // The escape hatch fires only for the UNRESOLVED payload field. Its own
        // sub-schema has no derivable shape (the ref dangled), so it collapses to
        // a permissive {} — and it must NOT carry the sibling shipTo's resolved
        // street/city fields (the root-schema leak this fix closes).
        Assert.DoesNotContain("street", jsonSchema!);
        Assert.DoesNotContain("city", jsonSchema!);
        Assert.DoesNotContain("shipTo", jsonSchema!);
        Assert.DoesNotContain("payload", jsonSchema!);
        Assert.DoesNotContain("$ref", jsonSchema!);
    }

    [Fact]
    public void EscapeHatch_WithResolvableSiblingObject_ScopesFieldEditorToItsOwnFragment()
    {
        // A fully RESOLVABLE sibling object (order → typed inline object) renders
        // typed inputs, NOT an escape hatch. Only the unresolvable payload drops
        // to Monaco — and its schema must be the field fragment, never the root
        // (which carries order's id/name).
        const string schema = """
            {
              "type": "object",
              "properties": {
                "order": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "integer" },
                    "name": { "type": "string" }
                  }
                },
                "payload": { "$ref": "lib:Missing" }
              }
            }
            """;

        var cut = Render<ParameterValueForm>(p => p
            .Add(x => x.ParameterDefinitions, schema)
            .Add(x => x.Values, new Dictionary<string, object?>()));

        // Exactly one escape-hatch editor (for payload); order renders typed inputs.
        var editor = cut.FindComponent<MonacoEditor>();
        var jsonSchema = editor.Instance.JsonSchema;

        Assert.False(string.IsNullOrWhiteSpace(jsonSchema));
        // Must not leak the sibling order field's declared shape.
        Assert.DoesNotContain("order", jsonSchema!);
        Assert.DoesNotContain("\"id\"", jsonSchema!);
    }

    [Fact]
    public void MultipleEscapeHatchFields_EachEditorReceivesItsOwnFieldFragment_NotRootSchema()
    {
        // TWO unresolvable-$ref fields each render their OWN escape-hatch Monaco
        // editor. Before the per-field fix, both received the SAME root object
        // schema (carrying both sibling field names) — this test would have caught
        // that. With the fix, each editor gets only its field's sub-schema (a
        // permissive {} for a dangling ref), carrying neither sibling's name.
        const string schema = """
            {
              "type": "object",
              "properties": {
                "left": { "$ref": "lib:MissingA" },
                "right": { "$ref": "lib:MissingB" }
              }
            }
            """;

        var cut = Render<ParameterValueForm>(p => p
            .Add(x => x.ParameterDefinitions, schema)
            .Add(x => x.Values, new Dictionary<string, object?>()));

        // FindComponents (plural): both unresolved-ref fields must render editors.
        var editors = cut.FindComponents<MonacoEditor>();
        Assert.Equal(2, editors.Count);

        foreach (var editor in editors)
        {
            var jsonSchema = editor.Instance.JsonSchema;
            Assert.False(string.IsNullOrWhiteSpace(jsonSchema));
            // Per-field fragment only — neither editor carries the root object's
            // property names (left/right) nor a residual $ref.
            Assert.DoesNotContain("left", jsonSchema!);
            Assert.DoesNotContain("right", jsonSchema!);
            Assert.DoesNotContain("$ref", jsonSchema!);
            Assert.DoesNotContain("properties", jsonSchema!);
        }
    }

    [Fact]
    public void MultipleEscapeHatchFields_DoNotLeakAResolvableSiblingsShape()
    {
        // The library entry "Address" resolves to street/city. The root mixes one
        // RESOLVABLE $ref (shipTo) with two UNRESOLVABLE ones (a, b). Each escape
        // hatch must be scoped to its own field — none may carry the resolved
        // street/city of the sibling shipTo (the precise root-schema leak).
        const string libSchema = """
            {
              "type": "object",
              "properties": {
                "street": { "type": "string" },
                "city": { "type": "string" }
              }
            }
            """;
        _library.GetSchemaMapAsync(Arg.Any<CancellationToken>())
            .Returns(new Dictionary<string, string> { ["Address"] = libSchema });

        const string schema = """
            {
              "type": "object",
              "properties": {
                "shipTo": { "$ref": "lib:Address" },
                "a": { "$ref": "lib:MissingA" },
                "b": { "$ref": "lib:MissingB" }
              }
            }
            """;

        var cut = Render<ParameterValueForm>(p => p
            .Add(x => x.ParameterDefinitions, schema)
            .Add(x => x.Values, new Dictionary<string, object?>()));

        // shipTo resolves to typed inputs; only a and b drop to escape hatches.
        var editors = cut.FindComponents<MonacoEditor>();
        Assert.Equal(2, editors.Count);

        foreach (var editor in editors)
        {
            var jsonSchema = editor.Instance.JsonSchema;
            Assert.False(string.IsNullOrWhiteSpace(jsonSchema));
            Assert.DoesNotContain("street", jsonSchema!);
            Assert.DoesNotContain("city", jsonSchema!);
            Assert.DoesNotContain("shipTo", jsonSchema!);
        }
    }
}
