using ZB.MOM.WW.ScadaBridge.Commons.Types.InboundApi;

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

/// <summary>
/// InboundAPI-014 / InboundAPI-M2.6: tests for return-value validation against a
/// method's <c>ReturnDefinition</c>. Mirrors <see cref="ParameterValidatorTests"/>
/// (shared recursive engine) — RECURSIVE nested Object / List-element type
/// validation with path-qualified errors.
///
/// <para>
/// Definitions are expressed as JSON Schema (the canonical persisted format);
/// the legacy flat-array form is still accepted (final region).
/// </para>
/// </summary>
public class ReturnValueValidatorTests
{
    // ── No definition → no validation (backward compatible) ───────────────────

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    public void NoReturnDefinition_AnythingIsValid(string? returnDefinition)
    {
        var result = ReturnValueValidator.Validate("{\"anything\":1}", returnDefinition);
        Assert.True(result.IsValid);
    }

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

    // ── Happy path: result matches the declared object shape ──────────────────

    [Fact]
    public void ResultMatchingDefinition_IsValid()
    {
        const string def = """
        {"type":"object","properties":{
            "siteName":{"type":"string"},
            "totalUnits":{"type":"integer"}
        },"required":["siteName","totalUnits"]}
        """;
        const string json = """{"siteName":"Site Alpha","totalUnits":14250}""";

        var result = ReturnValueValidator.Validate(json, def);

        Assert.True(result.IsValid);
    }

    [Fact]
    public void ResultWithListOfScalars_TypeChecked_IsValid()
    {
        const string def = """
        {"type":"object","properties":{
            "codes":{"type":"array","items":{"type":"integer"}}
        }}
        """;
        const string json = """{"codes":[1,2,3]}""";

        var result = ReturnValueValidator.Validate(json, def);

        Assert.True(result.IsValid);
    }

    // ── Scalar / shape mismatches must be reported ────────────────────────────

    [Fact]
    public void ResultMissingDeclaredField_IsInvalid()
    {
        const string def = """
        {"type":"object","properties":{
            "siteName":{"type":"string"},
            "totalUnits":{"type":"integer"}
        },"required":["siteName","totalUnits"]}
        """;
        const string json = """{"siteName":"Site Alpha"}""";

        var result = ReturnValueValidator.Validate(json, def);

        Assert.False(result.IsValid);
        Assert.Contains("totalUnits", result.ErrorMessage);
    }

    [Fact]
    public void ResultFieldWrongType_IsInvalid()
    {
        const string def = """{"type":"object","properties":{"totalUnits":{"type":"integer"}},"required":["totalUnits"]}""";
        const string json = """{"totalUnits":"not-a-number"}""";

        var result = ReturnValueValidator.Validate(json, def);

        Assert.False(result.IsValid);
        Assert.Contains("totalUnits", result.ErrorMessage);
    }

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

        var result = ReturnValueValidator.Validate(null, def);

        Assert.False(result.IsValid);
    }

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

        var result = ReturnValueValidator.Validate("42", def);

        Assert.False(result.IsValid);
    }

    [Fact]
    public void ListFieldGivenNonArray_IsInvalid()
    {
        const string def = """{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object"}}}}""";
        const string json = """{"lines":"not-a-list"}""";

        var result = ReturnValueValidator.Validate(json, def);

        Assert.False(result.IsValid);
        Assert.Contains("lines", result.ErrorMessage);
    }

    [Fact]
    public void MalformedReturnDefinition_IsInvalid()
    {
        var result = ReturnValueValidator.Validate("{\"x\":1}", "%%% not json %%%");

        Assert.False(result.IsValid);
    }

    // ── Nested validation: the M2.6 core (production-report shape) ─────────────

    private const string ReportDef = """
    {
      "type":"object",
      "properties":{
        "siteName":{"type":"string"},
        "totalUnits":{"type":"integer"},
        "lines":{
          "type":"array",
          "items":{
            "type":"object",
            "properties":{
              "lineName":{"type":"string"},
              "units":{"type":"integer"},
              "efficiency":{"type":"number"}
            },
            "required":["lineName","units"]
          }
        }
      },
      "required":["siteName","totalUnits","lines"]
    }
    """;

    [Fact]
    public void ValidNestedReturn_Passes()
    {
        const string json = """
        {
          "siteName":"Site Alpha",
          "totalUnits":14250,
          "lines":[
            {"lineName":"Line-1","units":8200,"efficiency":92.5},
            {"lineName":"Line-2","units":6050,"efficiency":88.1}
          ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.True(result.IsValid);
    }

    [Fact]
    public void WrongScalarInsideListElement_IsInvalid_WithElementIndexInPath()
    {
        // lines[1].units declared integer, given a string.
        const string json = """
        {
          "siteName":"Site Alpha",
          "totalUnits":14250,
          "lines":[
            {"lineName":"Line-1","units":8200},
            {"lineName":"Line-2","units":"lots"}
          ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.False(result.IsValid);
        Assert.Contains("'lines[1].units'", result.ErrorMessage);
        Assert.Contains("Integer", result.ErrorMessage);
    }

    [Fact]
    public void WrongListElementType_IsInvalid_WithElementIndexInPath()
    {
        // lines[0] declared object, given a scalar.
        const string json = """
        {
          "siteName":"Site Alpha",
          "totalUnits":14250,
          "lines":[ 7 ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.False(result.IsValid);
        Assert.Contains("'lines[0]'", result.ErrorMessage);
        Assert.Contains("Object", result.ErrorMessage);
    }

    [Fact]
    public void MissingRequiredNestedField_IsInvalid_PathQualified()
    {
        // lines[0].units is required but absent.
        const string json = """
        {
          "siteName":"Site Alpha",
          "totalUnits":14250,
          "lines":[ {"lineName":"Line-1"} ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.False(result.IsValid);
        Assert.Contains("missing required field", result.ErrorMessage);
        Assert.Contains("'lines[0].units'", result.ErrorMessage);
    }

    [Fact]
    public void UndeclaredNestedField_IsInvalid_PathQualified()
    {
        // lines[0].bogus is not declared on the line-item schema.
        const string json = """
        {
          "siteName":"Site Alpha",
          "totalUnits":14250,
          "lines":[ {"lineName":"Line-1","units":1,"bogus":true} ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.False(result.IsValid);
        Assert.Contains("'lines[0].bogus'", result.ErrorMessage);
        Assert.Contains("not a declared field", result.ErrorMessage);
    }

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

    [Fact]
    public void EmptyListAgainstTypedElement_Passes()
    {
        const string json = """{"siteName":"S","totalUnits":0,"lines":[]}""";

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.True(result.IsValid);
    }

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

        var result = ReturnValueValidator.Validate("""{"whatever":1}""", def);
        Assert.True(result.IsValid);
    }

    [Fact]
    public void NullOptionalNestedScalar_Passes()
    {
        // lines[0].efficiency is optional; explicit null is accepted.
        const string json = """
        {
          "siteName":"S",
          "totalUnits":1,
          "lines":[ {"lineName":"L1","units":1,"efficiency":null} ]
        }
        """;

        var result = ReturnValueValidator.Validate(json, ReportDef);
        Assert.True(result.IsValid);
    }

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

    [Fact]
    public void LegacyFlatArrayDefinition_StillAccepted()
    {
        const string def = """[{"name":"siteName","type":"String"},{"name":"totalUnits","type":"Integer"}]""";

        var ok = ReturnValueValidator.Validate("""{"siteName":"A","totalUnits":1}""", def);
        Assert.True(ok.IsValid);

        var bad = ReturnValueValidator.Validate("""{"siteName":"A","totalUnits":"x"}""", def);
        Assert.False(bad.IsValid);
        Assert.Contains("totalUnits", bad.ErrorMessage);
    }

    // FIX 3: scalar return schema validates scalar return values ──────────────
    // (Guards the intentional ParameterValidator/ReturnValueValidator asymmetry:
    //  ReturnValueValidator must NOT short-circuit on non-object schema types.)

    [Fact]
    public void ScalarStringReturnSchema_ValidatesScalarStringReturn()
    {
        // A {"type":"string"} return schema must accept a bare JSON string.
        var result = ReturnValueValidator.Validate("\"hello\"", """{"type":"string"}""");
        Assert.True(result.IsValid);
    }

    [Fact]
    public void ScalarIntegerReturnSchema_ValidatesScalarIntegerReturn()
    {
        var result = ReturnValueValidator.Validate("42", """{"type":"integer"}""");
        Assert.True(result.IsValid);
    }

    [Fact]
    public void ScalarStringReturnSchema_RejectsIntegerReturn()
    {
        var result = ReturnValueValidator.Validate("42", """{"type":"string"}""");
        Assert.False(result.IsValid);
        Assert.Contains("String", result.ErrorMessage);
    }

    [Fact]
    public void ScalarBooleanReturnSchema_ValidatesBooleanReturn()
    {
        var result = ReturnValueValidator.Validate("true", """{"type":"boolean"}""");
        Assert.True(result.IsValid);
    }

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

    [Fact]
    public void ValidateExceedingDepthCeiling_AddsDepthError_DoesNotThrow()
    {
        // Build a schema programmatically (bypassing Parse) with 34 levels of
        // nesting to exceed the ceiling of 32. Validate must add an error and
        // return, NOT stack overflow.
        //
        // Parse prevents creating a >32-level schema from stored JSON, but
        // InboundApiSchema is a public type constructable in code, so Validate
        // must guard independently.
        var deepSchema = BuildProgrammaticSchema(34);

        var json = BuildDeeplyNestedValue(34);
        using var doc = System.Text.Json.JsonDocument.Parse(json);

        var errors = new System.Collections.Generic.List<string>();
        // Must not throw — adds a depth error to the list instead.
        deepSchema.Validate(doc.RootElement, string.Empty, errors);

        Assert.NotEmpty(errors);
        Assert.Contains("nesting too deep", errors[0], StringComparison.OrdinalIgnoreCase);
    }

    /// <summary>
    /// Constructs an <see cref="InboundApiSchema"/> with <paramref name="depth"/>
    /// levels of object-nesting programmatically (bypassing <c>Parse</c>) to
    /// exercise the Validate depth ceiling independently of the Parse ceiling.
    /// </summary>
    private static InboundApiSchema BuildProgrammaticSchema(int depth)
    {
        InboundApiSchema inner = new() { Type = "string" };
        for (var i = 0; i < depth; i++)
        {
            inner = new InboundApiSchema
            {
                Type = "object",
                Fields = [new InboundApiSchemaField("a", Required: false, inner)],
            };
        }
        return inner;
    }

    private static string BuildDeeplyNestedValue(int depth)
    {
        var value = "\"leaf\"";
        for (var i = 0; i < depth; i++)
        {
            value = "{\"a\":" + value + "}";
        }
        return value;
    }
}
