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

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

/// <summary>
/// WP-2 / InboundAPI-M2.6: tests for parameter validation — type checking,
/// required fields, the extended type system, and RECURSIVE (nested Object /
/// List element) type validation with path-qualified errors.
///
/// <para>
/// Definitions are expressed as JSON Schema (the canonical persisted format
/// produced by the Central UI / migration). The validator also accepts the
/// legacy flat-array form; that backward-compat path is covered by the final
/// region.
/// </para>
/// </summary>
public class ParameterValidatorTests
{
    private static JsonElement Body(string json)
    {
        using var doc = JsonDocument.Parse(json);
        return doc.RootElement.Clone();
    }

    // ── No / empty definitions ────────────────────────────────────────────────

    [Fact]
    public void NoDefinitions_NoBody_ReturnsValid()
    {
        var result = ParameterValidator.Validate(null, null);
        Assert.True(result.IsValid);
        Assert.Empty(result.Parameters);
    }

    [Fact]
    public void EmptyObjectSchema_ReturnsValid()
    {
        var result = ParameterValidator.Validate(null, """{"type":"object","properties":{}}""");
        Assert.True(result.IsValid);
    }

    [Fact]
    public void EmptyLegacyArray_ReturnsValid()
    {
        var result = ParameterValidator.Validate(null, "[]");
        Assert.True(result.IsValid);
    }

    // ── Required / body shape ──────────────────────────────────────────────────

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

        var result = ParameterValidator.Validate(null, def);
        Assert.False(result.IsValid);
        Assert.Contains("Missing required parameter", result.ErrorMessage);
    }

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

        var result = ParameterValidator.Validate(Body("\"just a string\""), def);
        Assert.False(result.IsValid);
        Assert.Contains("must be a JSON object", result.ErrorMessage);
    }

    [Fact]
    public void OptionalParameter_MissingBody_ReturnsValid()
    {
        const string def = """{"type":"object","properties":{"optional":{"type":"string"}}}""";

        var result = ParameterValidator.Validate(null, def);
        Assert.True(result.IsValid);
    }

    // ── Scalar coercion ────────────────────────────────────────────────────────

    [Theory]
    [InlineData("boolean", "true", true)]
    [InlineData("integer", "42", (long)42)]
    [InlineData("number", "3.14", 3.14)]
    [InlineData("string", "\"hello\"", "hello")]
    public void ValidTypeCoercion_Succeeds(string type, string jsonValue, object expected)
    {
        var def = "{\"type\":\"object\",\"properties\":{\"val\":{\"type\":\"" + type + "\"}},\"required\":[\"val\"]}";

        var result = ParameterValidator.Validate(Body($"{{\"val\": {jsonValue}}}"), def);
        Assert.True(result.IsValid);
        Assert.Equal(expected, result.Parameters["val"]);
    }

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

        var result = ParameterValidator.Validate(Body("{\"count\": \"not a number\"}"), def);
        Assert.False(result.IsValid);
        Assert.Contains("'count'", result.ErrorMessage);
        Assert.Contains("Integer", result.ErrorMessage);
    }

    [Fact]
    public void UnknownType_ReturnsInvalid()
    {
        const string def = """{"type":"object","properties":{"val":{"type":"customtype"}},"required":["val"]}""";

        var result = ParameterValidator.Validate(Body("{\"val\": \"test\"}"), def);
        Assert.False(result.IsValid);
        Assert.Contains("unknown declared type", result.ErrorMessage);
    }

    // ── Object / List shape + materialization ──────────────────────────────────

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

        var result = ParameterValidator.Validate(Body("{\"data\": {\"key\": \"value\"}}"), def);
        Assert.True(result.IsValid);
        Assert.IsType<Dictionary<string, object?>>(result.Parameters["data"]);
    }

    [Fact]
    public void ListType_NoDeclaredElement_ShapeOnly_Materialized()
    {
        const string def = """{"type":"object","properties":{"items":{"type":"array"}},"required":["items"]}""";

        var result = ParameterValidator.Validate(Body("{\"items\": [1, 2, 3]}"), def);
        Assert.True(result.IsValid);
        Assert.IsType<List<object?>>(result.Parameters["items"]);
    }

    // ── Undeclared / unexpected fields (rejected, recursively) ─────────────────

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

        // "valeu" is a typo for "value"; the caller must be told, not ignored.
        var result = ParameterValidator.Validate(Body("{\"value\": 1, \"valeu\": 2}"), def);

        Assert.False(result.IsValid);
        Assert.Contains("valeu", result.ErrorMessage);
        Assert.Contains("not a declared field", result.ErrorMessage);
    }

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

        var result = ParameterValidator.Validate(Body("{\"value\": 1}"), def);

        Assert.True(result.IsValid);
        Assert.Equal((long)1, result.Parameters["value"]);
    }

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

        var result = ParameterValidator.Validate(
            Body("""{"order":{"id":1,"bogus":2}}"""), def);

        Assert.False(result.IsValid);
        Assert.Contains("order.bogus", result.ErrorMessage);
        Assert.Contains("not a declared field", result.ErrorMessage);
    }

    // ── Nested validation: the M2.6 core ───────────────────────────────────────

    private const string NestedDef = """
    {
      "type":"object",
      "properties":{
        "order":{
          "type":"object",
          "properties":{
            "id":{"type":"integer"},
            "customer":{
              "type":"object",
              "properties":{"name":{"type":"string"},"vip":{"type":"boolean"}},
              "required":["name"]
            },
            "items":{
              "type":"array",
              "items":{
                "type":"object",
                "properties":{"sku":{"type":"string"},"quantity":{"type":"integer"}},
                "required":["sku","quantity"]
              }
            }
          },
          "required":["id","customer","items"]
        }
      },
      "required":["order"]
    }
    """;

    [Fact]
    public void ValidNestedPayload_Passes()
    {
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme","vip":true},
            "items":[
                {"sku":"A1","quantity":3},
                {"sku":"B2","quantity":1}
            ]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.True(result.IsValid);
    }

    [Fact]
    public void WrongScalarTwoLevelsDeep_ReturnsInvalid_WithExactPath()
    {
        // order.customer.vip declared boolean, given a string.
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme","vip":"yes"},
            "items":[]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.False(result.IsValid);
        Assert.Contains("'order.customer.vip'", result.ErrorMessage);
        Assert.Contains("Boolean", result.ErrorMessage);
    }

    [Fact]
    public void WrongScalarInsideListElement_ReturnsInvalid_WithElementIndexInPath()
    {
        // order.items[1].quantity declared integer, given a string.
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme"},
            "items":[
                {"sku":"A1","quantity":3},
                {"sku":"B2","quantity":"lots"}
            ]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.False(result.IsValid);
        Assert.Contains("'order.items[1].quantity'", result.ErrorMessage);
        Assert.Contains("Integer", result.ErrorMessage);
    }

    [Fact]
    public void ListElementWrongShape_ReturnsInvalid_WithElementIndexInPath()
    {
        // order.items[0] declared object, given a scalar.
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme"},
            "items":[ 42 ]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.False(result.IsValid);
        Assert.Contains("'order.items[0]'", result.ErrorMessage);
        Assert.Contains("Object", result.ErrorMessage);
    }

    [Fact]
    public void MissingRequiredNestedField_ReturnsInvalid_PathQualified()
    {
        // order.customer.name is required but absent.
        const string body = """
        {"order":{
            "id":7,
            "customer":{"vip":false},
            "items":[]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.False(result.IsValid);
        Assert.Contains("missing required field", result.ErrorMessage);
        Assert.Contains("'order.customer.name'", result.ErrorMessage);
    }

    // ── Empty / null edge cases ────────────────────────────────────────────────

    [Fact]
    public void EmptyList_AgainstTypedElement_Passes()
    {
        const string body = """
        {"order":{"id":7,"customer":{"name":"Acme"},"items":[]}}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.True(result.IsValid);
    }

    [Fact]
    public void NullForOptionalNestedScalar_Passes()
    {
        // order.customer.vip is optional; explicit null is accepted.
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme","vip":null},
            "items":[]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.True(result.IsValid);
    }

    [Fact]
    public void NullForRequiredNestedScalar_Passes()
    {
        // A PRESENT-but-null required field satisfies the type — only ABSENCE
        // of a required field is an error (consistent with return-side policy).
        const string body = """
        {"order":{
            "id":null,
            "customer":{"name":"Acme"},
            "items":[]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.True(result.IsValid);
    }

    // ── Recursive materialization: nested values coerced to typed CLR (#55) ─────

    /// <summary>
    /// Recursively asserts that no value anywhere in a materialized parameter
    /// graph is a raw <see cref="JsonElement"/> — every scalar must be a typed CLR
    /// value (string/long/double/bool/null) and every container a
    /// <c>Dictionary&lt;string, object?&gt;</c> / <c>List&lt;object?&gt;</c> (or a
    /// typed scalar list) of likewise-coerced values.
    /// </summary>
    private static void AssertNoJsonElement(object? value)
    {
        Assert.IsNotType<JsonElement>(value);

        switch (value)
        {
            case IDictionary<string, object?> dict:
                foreach (var v in dict.Values)
                {
                    AssertNoJsonElement(v);
                }

                break;
            case System.Collections.IEnumerable enumerable when value is not string:
                foreach (var item in enumerable)
                {
                    AssertNoJsonElement(item);
                }

                break;
        }
    }

    [Fact]
    public void NestedObjectFieldScalar_IsTypedClr_NotJsonElement()
    {
        const string body = """
        {"order":{
            "id":7,
            "customer":{"name":"Acme","vip":true},
            "items":[]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), NestedDef);
        Assert.True(result.IsValid);

        var order = Assert.IsType<Dictionary<string, object?>>(result.Parameters["order"]);
        AssertNoJsonElement(order);

        // The nested scalar fields are the CLR types the top-level path produces:
        // integer→long, string→string, boolean→bool.
        Assert.Equal((long)7, order["id"]);

        var customer = Assert.IsType<Dictionary<string, object?>>(order["customer"]);
        Assert.Equal("Acme", customer["name"]);
        Assert.Equal(true, customer["vip"]);
        Assert.IsType<bool>(customer["vip"]);
        Assert.IsType<long>(order["id"]);
    }

    [Fact]
    public void ListOfScalars_FullyCoerced_NoJsonElement()
    {
        const string def = """{"type":"object","properties":{"nums":{"type":"array","items":{"type":"integer"}}},"required":["nums"]}""";

        var result = ParameterValidator.Validate(Body("{\"nums\":[1,2,3]}"), def);
        Assert.True(result.IsValid);

        // Declared scalar element → strongly-typed List<long>.
        var nums = Assert.IsType<List<long>>(result.Parameters["nums"]);
        Assert.Equal(new List<long> { 1, 2, 3 }, nums);
        AssertNoJsonElement(nums);
    }

    [Fact]
    public void ListOfObjects_ElementFieldsCoerced_NoJsonElement()
    {
        const string def = """
        {"type":"object","properties":{
            "items":{"type":"array","items":{
                "type":"object",
                "properties":{"sku":{"type":"string"},"quantity":{"type":"integer"},"price":{"type":"number"}},
                "required":["sku","quantity"]
            }}
        },"required":["items"]}
        """;

        const string body = """{"items":[{"sku":"A1","quantity":3,"price":9.5},{"sku":"B2","quantity":1,"price":2.0}]}""";

        var result = ParameterValidator.Validate(Body(body), def);
        Assert.True(result.IsValid);

        var items = Assert.IsType<List<object?>>(result.Parameters["items"]);
        AssertNoJsonElement(items);

        var first = Assert.IsType<Dictionary<string, object?>>(items[0]);
        Assert.Equal("A1", first["sku"]);
        Assert.Equal((long)3, first["quantity"]);
        Assert.IsType<long>(first["quantity"]);
        Assert.Equal(9.5, first["price"]);
        Assert.IsType<double>(first["price"]);
    }

    [Fact]
    public void DeeplyNested_ObjectInListInObject_FullyCoerced_NoJsonElement()
    {
        // order.items[*] is an object that itself carries a list of tag objects:
        // object → array → object → scalar, the worst-case leak path.
        const string def = """
        {"type":"object","properties":{
            "order":{"type":"object","properties":{
                "id":{"type":"integer"},
                "items":{"type":"array","items":{
                    "type":"object","properties":{
                        "sku":{"type":"string"},
                        "tags":{"type":"array","items":{
                            "type":"object","properties":{
                                "key":{"type":"string"},
                                "weight":{"type":"number"},
                                "active":{"type":"boolean"}
                            },"required":["key"]
                        }}
                    },"required":["sku"]
                }}
            },"required":["id","items"]}
        },"required":["order"]}
        """;

        const string body = """
        {"order":{
            "id":7,
            "items":[
                {"sku":"A1","tags":[{"key":"hot","weight":1.5,"active":true},{"key":"cold","weight":0.25,"active":false}]}
            ]
        }}
        """;

        var result = ParameterValidator.Validate(Body(body), def);
        Assert.True(result.IsValid);

        // The whole graph must be free of JsonElement at every depth.
        AssertNoJsonElement(result.Parameters["order"]);

        var order = Assert.IsType<Dictionary<string, object?>>(result.Parameters["order"]);
        Assert.Equal((long)7, order["id"]);

        var items = Assert.IsType<List<object?>>(order["items"]);
        var item0 = Assert.IsType<Dictionary<string, object?>>(items[0]);
        Assert.Equal("A1", item0["sku"]);

        var tags = Assert.IsType<List<object?>>(item0["tags"]);
        var tag0 = Assert.IsType<Dictionary<string, object?>>(tags[0]);
        Assert.Equal("hot", tag0["key"]);
        Assert.Equal(1.5, tag0["weight"]);
        Assert.IsType<double>(tag0["weight"]);
        Assert.Equal(true, tag0["active"]);
        Assert.IsType<bool>(tag0["active"]);

        var tag1 = Assert.IsType<Dictionary<string, object?>>(tags[1]);
        Assert.Equal(false, tag1["active"]);
    }

    [Fact]
    public void ShapeOnlyNestedObject_StillFullyCoerced_NoJsonElement()
    {
        // A declared-but-shape-only object field ({"type":"object"} with no
        // properties) is still coerced structurally — no JsonElement survives.
        // Without a declared field schema there is no integer/float distinction,
        // so (per the pre-existing structural-coercion path) numbers come back as
        // double; the point of THIS test is the absence of JsonElement, not the
        // integer-vs-double choice (the declared paths cover that).
        const string def = """{"type":"object","properties":{"data":{"type":"object"}},"required":["data"]}""";

        const string body = """{"data":{"name":"x","count":5,"ratio":1.5,"flag":true,"nested":{"inner":2}}}""";

        var result = ParameterValidator.Validate(Body(body), def);
        Assert.True(result.IsValid);

        var data = Assert.IsType<Dictionary<string, object?>>(result.Parameters["data"]);
        AssertNoJsonElement(data);
        Assert.Equal("x", data["name"]);
        Assert.Equal(5d, data["count"]);
        Assert.Equal(1.5, data["ratio"]);
        Assert.Equal(true, data["flag"]);
        var nested = Assert.IsType<Dictionary<string, object?>>(data["nested"]);
        Assert.Equal(2d, nested["inner"]);
    }

    // ── Legacy flat-array backward-compat ──────────────────────────────────────

    [Fact]
    public void LegacyFlatArrayDefinition_StillAccepted()
    {
        const string def = """[{"name":"count","type":"Integer","required":true}]""";

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

        var bad = ParameterValidator.Validate(Body("{\"count\":\"nope\"}"), def);
        Assert.False(bad.IsValid);
        Assert.Contains("'count'", bad.ErrorMessage);
    }

    // FIX 1: legacy "required":"false" string → field is optional ─────────────

    [Theory]
    [InlineData("""[{"name":"opt","type":"String","required":"false"}]""")]
    [InlineData("""[{"name":"opt","type":"String","required":"False"}]""")]
    [InlineData("""[{"name":"opt","type":"String","required":"FALSE"}]""")]
    public void LegacyFlatArray_RequiredStringFalse_FieldIsOptional(string def)
    {
        // An absent field whose "required" is the string "false" (any case)
        // must be treated as optional — consistent with the SQL migration's
        // LOWER(...) <> 'false' comparison that produced these rows.
        var result = ParameterValidator.Validate(null, def);
        Assert.True(result.IsValid, $"Expected optional field to be valid when absent; error: {result.ErrorMessage}");
    }

    [Fact]
    public void LegacyFlatArray_RequiredStringFalse_FieldPresentAndTypedCorrectly_Passes()
    {
        const string def = """[{"name":"opt","type":"String","required":"false"}]""";

        var result = ParameterValidator.Validate(Body("{\"opt\":\"hello\"}"), def);
        Assert.True(result.IsValid);
    }

    // FIX 2: recursion depth guard on Parse ───────────────────────────────────

    /// <summary>
    /// Builds a JSON Schema string with <paramref name="depth"/> levels of nested
    /// object-in-properties nesting. Each level wraps the previous in an object
    /// with a single property "a". The result exceeds the Parse ceiling when
    /// depth &gt; 32.
    /// </summary>
    private static string BuildDeeplyNestedSchema(int depth)
    {
        // Inner-most: a scalar
        var schema = "{\"type\":\"string\"}";
        for (var i = 0; i < depth; i++)
        {
            schema = "{\"type\":\"object\",\"properties\":{\"a\":" + schema + "}}";
        }
        return schema;
    }

    [Fact]
    public void SchemaAtDepthCeiling_ParsesSuccessfully()
    {
        // Exactly 32 levels of nesting should parse without throwing.
        var def = BuildDeeplyNestedSchema(32);
        var schema = InboundApiSchema.Parse(def);
        Assert.NotNull(schema);
    }

    [Fact]
    public void SchemaExceedingDepthCeiling_ThrowsJsonException_NotStackOverflow()
    {
        // 33 levels exceeds the ceiling → JsonException (clean 400 via the
        // caller's try/catch), NOT a StackOverflowException.
        var def = BuildDeeplyNestedSchema(33);
        Assert.Throws<System.Text.Json.JsonException>(() => InboundApiSchema.Parse(def));
    }

    [Fact]
    public void SchemaExceedingDepthCeiling_ParameterValidator_ReturnsInvalid()
    {
        // End-to-end: ParameterValidator wraps Parse in try/catch(JsonException)
        // → the caller gets Invalid rather than an unhandled exception.
        var def = BuildDeeplyNestedSchema(33);
        var result = ParameterValidator.Validate(Body("{\"a\":\"x\"}"), def);
        Assert.False(result.IsValid);
        Assert.Contains("Invalid parameter definitions", result.ErrorMessage);
    }
}
