using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns;

/// <summary>
/// Tests for the typed AdminUI Sql tag-config model. The load-bearing property is editor-output ⇔
/// parser-input agreement: what <see cref="SqlTagConfigModel.ToJson"/> writes MUST parse cleanly through
/// <see cref="SqlEquipmentTagParser.TryParse"/> (the runtime factory's authoritative reader), and the
/// <c>model</c>/<c>type</c> enums MUST serialise as NAME strings — a numeric enum makes the parser reject
/// the tag (the "authors fine, never reads" defect class this guards).
/// </summary>
public sealed class SqlTagConfigModelTests
{
    // ---- the plan's required assertion: string enums + unknown-key survival --------------------

    [Fact]
    public void ToJson_writesModelAndTypeAsStrings_preservesUnknownKeys()
    {
        var m = SqlTagConfigModel.FromJson(
            """{"driver":"Sql","model":"KeyValue","table":"t","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"Float64","customX":1}""");
        var json = m.ToJson();
        json.ShouldContain("\"KeyValue\"");
        json.ShouldContain("\"Float64\"");
        json.ShouldContain("customX");            // unknown key survives load→save
        json.ShouldNotContain("\"model\":0");
        json.ShouldNotContain("\"type\":8");      // no numeric enum leaks
    }

    // ---- defaults ------------------------------------------------------------------------------

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("{}")]
    public void FromJson_returns_defaults_for_empty_input(string? json)
    {
        var m = SqlTagConfigModel.FromJson(json);

        m.Model.ShouldBe(SqlTagModel.KeyValue);
        m.Table.ShouldBeNull();
        m.KeyColumn.ShouldBeNull();
        m.KeyValue.ShouldBeNull();
        m.ValueColumn.ShouldBeNull();
        m.TimestampColumn.ShouldBeNull();
        m.ColumnName.ShouldBeNull();
        m.Type.ShouldBeNull();
        m.RowSelector.WhereColumn.ShouldBeNull();
        m.RowSelector.WhereValue.ShouldBeNull();
        m.RowSelector.TopByTimestamp.ShouldBeNull();
    }

    [Fact]
    public void ToJson_of_default_model_omits_optional_keys()
    {
        var json = new SqlTagConfigModel { Table = "t" }.ToJson();

        json.ShouldContain("\"model\":\"KeyValue\"");
        json.ShouldContain("\"table\":\"t\"");
        json.ShouldNotContain("type");            // no type set ⇒ key omitted
        json.ShouldNotContain("rowSelector");     // no selector ⇒ key omitted
        json.ShouldNotContain("timestampColumn");
    }

    // ---- round-trip fidelity through FromJson→ToJson→FromJson ----------------------------------

    [Fact]
    public void Round_trip_preserves_keyValue_fields()
    {
        var m = new SqlTagConfigModel
        {
            Model = SqlTagModel.KeyValue,
            Table = "dbo.TagValues",
            KeyColumn = "TagName",
            KeyValue = "Line1.Speed",
            ValueColumn = "Val",
            TimestampColumn = "Ts",
            Type = DriverDataType.Float64,
        };

        var m2 = SqlTagConfigModel.FromJson(m.ToJson());

        m2.Model.ShouldBe(SqlTagModel.KeyValue);
        m2.Table.ShouldBe("dbo.TagValues");
        m2.KeyColumn.ShouldBe("TagName");
        m2.KeyValue.ShouldBe("Line1.Speed");
        m2.ValueColumn.ShouldBe("Val");
        m2.TimestampColumn.ShouldBe("Ts");
        m2.Type.ShouldBe(DriverDataType.Float64);
    }

    [Fact]
    public void Round_trip_preserves_wideRow_wherePair_fields()
    {
        var m = new SqlTagConfigModel
        {
            Model = SqlTagModel.WideRow,
            Table = "dbo.Wide",
            ColumnName = "Speed",
            RowSelector = new SqlRowSelectorModel { WhereColumn = "DeviceId", WhereValue = "42" },
        };

        var m2 = SqlTagConfigModel.FromJson(m.ToJson());

        m2.Model.ShouldBe(SqlTagModel.WideRow);
        m2.Table.ShouldBe("dbo.Wide");
        m2.ColumnName.ShouldBe("Speed");
        m2.RowSelector.WhereColumn.ShouldBe("DeviceId");
        m2.RowSelector.WhereValue.ShouldBe("42");
        m2.RowSelector.TopByTimestamp.ShouldBeNull();
    }

    [Fact]
    public void Round_trip_preserves_wideRow_topByTimestamp_fields()
    {
        var m = new SqlTagConfigModel
        {
            Model = SqlTagModel.WideRow,
            Table = "dbo.Wide",
            ColumnName = "Speed",
            RowSelector = new SqlRowSelectorModel { TopByTimestamp = "Ts" },
        };

        var m2 = SqlTagConfigModel.FromJson(m.ToJson());

        m2.Model.ShouldBe(SqlTagModel.WideRow);
        m2.ColumnName.ShouldBe("Speed");
        m2.RowSelector.TopByTimestamp.ShouldBe("Ts");
        m2.RowSelector.WhereColumn.ShouldBeNull();
        m2.RowSelector.WhereValue.ShouldBeNull();
    }

    [Fact]
    public void FromJson_then_ToJson_preserves_unknown_keys_top_level_and_nested()
    {
        var json = SqlTagConfigModel
            .FromJson(
                """{"model":"WideRow","table":"t","columnName":"c","rowSelector":{"topByTimestamp":"Ts","futureKey":"keep"},"scaling":2.5}""")
            .ToJson();

        json.ShouldContain("scaling");            // unknown top-level key survives
        json.ShouldContain("2.5");
        json.ShouldContain("futureKey");          // unknown nested (rowSelector) key survives
        json.ShouldContain("keep");
        json.ShouldContain("\"topByTimestamp\":\"Ts\"");
    }

    // ---- Validate() mirrors the parser's accept/reject boundary --------------------------------

    [Fact]
    public void Validate_returns_null_for_valid_keyValue()
        => SqlTagConfigModel.FromJson(
            """{"model":"KeyValue","table":"t","keyColumn":"k","keyValue":"v","valueColumn":"c"}""")
            .Validate().ShouldBeNull();

    [Fact]
    public void Validate_returns_null_for_valid_wideRow_wherePair()
        => SqlTagConfigModel.FromJson(
            """{"model":"WideRow","table":"t","columnName":"c","rowSelector":{"whereColumn":"id","whereValue":"1"}}""")
            .Validate().ShouldBeNull();

    [Fact]
    public void Validate_returns_null_for_valid_wideRow_topByTimestamp()
        => SqlTagConfigModel.FromJson(
            """{"model":"WideRow","table":"t","columnName":"c","rowSelector":{"topByTimestamp":"Ts"}}""")
            .Validate().ShouldBeNull();

    [Fact]
    public void Validate_rejects_missing_table()
        => SqlTagConfigModel.FromJson("""{"model":"KeyValue","keyColumn":"k","keyValue":"v","valueColumn":"c"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_keyValue_missing_valueColumn()
        => SqlTagConfigModel.FromJson("""{"model":"KeyValue","table":"t","keyColumn":"k","keyValue":"v"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_keyValue_missing_keyValue_field()
        => SqlTagConfigModel.FromJson("""{"model":"KeyValue","table":"t","keyColumn":"k","valueColumn":"c"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_wideRow_without_a_selector()
        => SqlTagConfigModel.FromJson("""{"model":"WideRow","table":"t","columnName":"c"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_wideRow_missing_columnName()
        => SqlTagConfigModel.FromJson(
            """{"model":"WideRow","table":"t","rowSelector":{"topByTimestamp":"Ts"}}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_invalid_model_enum()
        => SqlTagConfigModel.FromJson("""{"model":"Bogus","table":"t"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_invalid_type_enum()
        => SqlTagConfigModel.FromJson(
            """{"model":"KeyValue","table":"t","keyColumn":"k","keyValue":"v","valueColumn":"c","type":"Bogus"}""")
            .Validate().ShouldNotBeNull();

    [Fact]
    public void Validate_rejects_deferred_query_model()
        => SqlTagConfigModel.FromJson("""{"model":"Query","table":"t"}""")
            .Validate().ShouldNotBeNull();

    // ---- LOAD-BEARING: editor output MUST parse through the runtime factory reader --------------

    [Theory]
    [InlineData("""{"model":"KeyValue","table":"dbo.TagValues","keyColumn":"TagName","keyValue":"Line1.Speed","valueColumn":"Val","timestampColumn":"Ts","type":"Float64"}""")]
    [InlineData("""{"model":"WideRow","table":"dbo.Wide","columnName":"Speed","rowSelector":{"whereColumn":"DeviceId","whereValue":"42"}}""")]
    [InlineData("""{"model":"WideRow","table":"dbo.Wide","columnName":"Speed","rowSelector":{"topByTimestamp":"Ts"}}""")]
    public void ToJson_output_parses_cleanly_through_SqlEquipmentTagParser(string authored)
    {
        // Round-trip the authored blob through the editor model, exactly as the TagModal save path does.
        var roundTripped = SqlTagConfigModel.FromJson(authored).ToJson();

        // The authoritative runtime reader must accept it — this is the editor-output ⇔ parser-input contract.
        SqlEquipmentTagParser.TryParse(roundTripped, "Plant/Sql/dev1/Speed", out var def).ShouldBeTrue();
        def.Name.ShouldBe("Plant/Sql/dev1/Speed");
        def.Table.ShouldNotBeNullOrWhiteSpace();
    }

    [Fact]
    public void ToJson_keyValue_output_parses_to_expected_definition_fields()
    {
        var json = SqlTagConfigModel.FromJson(
            """{"model":"KeyValue","table":"dbo.TagValues","keyColumn":"TagName","keyValue":"Line1.Speed","valueColumn":"Val","timestampColumn":"Ts","type":"Float64"}""")
            .ToJson();

        SqlEquipmentTagParser.TryParse(json, "raw/path", out var def).ShouldBeTrue();
        def.Model.ShouldBe(SqlTagModel.KeyValue);
        def.Table.ShouldBe("dbo.TagValues");
        def.KeyColumn.ShouldBe("TagName");
        def.KeyValue.ShouldBe("Line1.Speed");
        def.ValueColumn.ShouldBe("Val");
        def.TimestampColumn.ShouldBe("Ts");
        def.DeclaredType.ShouldBe(DriverDataType.Float64);
    }

    [Fact]
    public void ToJson_wideRow_wherePair_output_parses_to_expected_definition_fields()
    {
        var json = SqlTagConfigModel.FromJson(
            """{"model":"WideRow","table":"dbo.Wide","columnName":"Speed","rowSelector":{"whereColumn":"DeviceId","whereValue":"42"}}""")
            .ToJson();

        SqlEquipmentTagParser.TryParse(json, "raw/path", out var def).ShouldBeTrue();
        def.Model.ShouldBe(SqlTagModel.WideRow);
        def.ColumnName.ShouldBe("Speed");
        def.RowSelectorColumn.ShouldBe("DeviceId");
        def.RowSelectorValue.ShouldBe("42");
        def.RowSelectorTopByTimestamp.ShouldBeNull();
    }

    [Fact]
    public void ToJson_wideRow_topByTimestamp_output_parses_to_expected_definition_fields()
    {
        var json = SqlTagConfigModel.FromJson(
            """{"model":"WideRow","table":"dbo.Wide","columnName":"Speed","rowSelector":{"topByTimestamp":"Ts"}}""")
            .ToJson();

        SqlEquipmentTagParser.TryParse(json, "raw/path", out var def).ShouldBeTrue();
        def.Model.ShouldBe(SqlTagModel.WideRow);
        def.ColumnName.ShouldBe("Speed");
        def.RowSelectorTopByTimestamp.ShouldBe("Ts");
        def.RowSelectorColumn.ShouldBeNull();
        def.RowSelectorValue.ShouldBeNull();
    }
}
