using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;

namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests;

/// <summary>
/// M9-T32b runtime regression: the InboundAPI RUNTIME validators
/// (<see cref="ParameterValidator"/> / <see cref="ReturnValueValidator"/>) must
/// resolve <c>{"$ref":"lib:Name"}</c> library references through the same seam the
/// deploy-time path uses. Before this fix the runtime validators called the
/// single-arg <c>InboundApiSchema.Parse(json)</c> with NO resolver, so a schema
/// containing a <c>$ref</c> threw <see cref="JsonException"/> and every runtime
/// invocation of the method returned a 400 ("Invalid ... definitions ...") — a
/// deploy-passes/runtime-breaks defect.
///
/// <para>
/// These tests assert: a VALID ref resolves and validation proceeds against the
/// referenced shape; a DANGLING ref yields a CLEAR Invalid result naming the
/// missing reference (NOT an opaque "Invalid ... definitions" from a swallowed
/// <see cref="JsonException"/>); and schemas WITHOUT a <c>$ref</c> are unaffected
/// when no resolver is supplied.
/// </para>
/// </summary>
public class SchemaRefRuntimeValidationTests
{
    private static JsonElement Body(string json)
    {
        using var doc = JsonDocument.Parse(json);
        return doc.RootElement.Clone();
    }

    // ── ParameterValidator: $ref in parameter definitions ─────────────────────

    [Fact]
    public void ParameterValidator_ValidLibRef_ResolvesAndValidates()
    {
        // The parameter schema is a single {"$ref":"lib:OrderRequest"} that the
        // resolver maps to an object schema with one required integer field.
        const string orderRequest =
            """{"type":"object","properties":{"qty":{"type":"integer"}},"required":["qty"]}""";
        const string paramDef = """{"$ref":"lib:OrderRequest"}""";

        var ok = ParameterValidator.Validate(
            Body("""{"qty":5}"""),
            paramDef,
            resolveRef: name => name == "OrderRequest" ? orderRequest : null);

        Assert.True(ok.IsValid, ok.ErrorMessage);
        Assert.Equal(5L, ok.Parameters["qty"]);

        // And a body that violates the resolved shape is rejected with a real
        // type error (NOT a configuration/parse error) — proving the ref shape
        // is actually being enforced.
        var bad = ParameterValidator.Validate(
            Body("""{"qty":"not-a-number"}"""),
            paramDef,
            resolveRef: name => name == "OrderRequest" ? orderRequest : null);

        Assert.False(bad.IsValid);
        Assert.DoesNotContain("definitions in method configuration", bad.ErrorMessage);
    }

    [Fact]
    public void ParameterValidator_DanglingLibRef_ReturnsClearInvalidNamingTheRef()
    {
        // The resolver cannot find "GoneLib" — at runtime this must surface as a
        // CLEAR Invalid result that names the missing reference, NOT an opaque
        // "Invalid parameter definitions" and NOT a thrown JsonException.
        const string paramDef = """{"$ref":"lib:GoneLib"}""";

        var result = ParameterValidator.Validate(
            Body("""{"anything":1}"""),
            paramDef,
            resolveRef: _ => null);

        Assert.False(result.IsValid);
        Assert.NotNull(result.ErrorMessage);
        Assert.Contains("GoneLib", result.ErrorMessage!, StringComparison.Ordinal);
    }

    [Fact]
    public void ParameterValidator_NoRef_UnchangedWithoutResolver()
    {
        // A schema with no $ref behaves exactly as before when no resolver is
        // supplied (the default-null resolver path).
        const string def =
            """{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}""";

        var ok = ParameterValidator.Validate(Body("""{"count":3}"""), def);
        Assert.True(ok.IsValid, ok.ErrorMessage);
        Assert.Equal(3L, ok.Parameters["count"]);

        var bad = ParameterValidator.Validate(Body("""{"count":"x"}"""), def);
        Assert.False(bad.IsValid);
    }

    // ── ReturnValueValidator: $ref in return definition ───────────────────────

    [Fact]
    public void ReturnValueValidator_ValidLibRef_ResolvesAndValidates()
    {
        const string report =
            """{"type":"object","properties":{"total":{"type":"integer"}},"required":["total"]}""";
        const string returnDef = """{"$ref":"lib:Report"}""";

        var ok = ReturnValueValidator.Validate(
            """{"total":42}""",
            returnDef,
            resolveRef: name => name == "Report" ? report : null);

        Assert.True(ok.IsValid, ok.ErrorMessage);

        // A return value violating the resolved shape is rejected as a real
        // validation failure, not a configuration error.
        var bad = ReturnValueValidator.Validate(
            """{"total":"nope"}""",
            returnDef,
            resolveRef: name => name == "Report" ? report : null);

        Assert.False(bad.IsValid);
        Assert.DoesNotContain("definition in method configuration", bad.ErrorMessage);
    }

    [Fact]
    public void ReturnValueValidator_DanglingLibRef_ReturnsClearInvalidNamingTheRef()
    {
        const string returnDef = """{"$ref":"lib:GoneReturn"}""";

        var result = ReturnValueValidator.Validate(
            """{"total":1}""",
            returnDef,
            resolveRef: _ => null);

        Assert.False(result.IsValid);
        Assert.Contains("GoneReturn", result.ErrorMessage, StringComparison.Ordinal);
    }

    [Fact]
    public void ReturnValueValidator_NoRef_UnchangedWithoutResolver()
    {
        const string def =
            """{"type":"object","properties":{"total":{"type":"integer"}},"required":["total"]}""";

        var ok = ReturnValueValidator.Validate("""{"total":7}""", def);
        Assert.True(ok.IsValid, ok.ErrorMessage);

        var bad = ReturnValueValidator.Validate("""{"total":"x"}""", def);
        Assert.False(bad.IsValid);
    }
}
