using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;

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

/// <summary>
/// Unit tests for the driver-agnostic <see cref="TagArrayConfig"/> merge helper — the pure seam the
/// TagModal uses to read/write the root <c>isArray</c> / <c>arrayLength</c> array keys on the canonical
/// TagConfig JSON WITHOUT disturbing the driver's own (typed-editor) fields. Both this helper and every
/// typed editor preserve unknown keys, so they compose over the same JSON blob. Mirrors
/// <see cref="TagHistorizeConfigTests"/>.
/// </summary>
public sealed class TagArrayConfigTests
{
    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("{}")]
    [InlineData("not json")]
    public void Read_returns_defaults_for_empty_or_malformed(string? json)
    {
        var a = TagArrayConfig.Read(json);

        a.IsArray.ShouldBeFalse();
        a.ArrayLength.ShouldBeNull();
    }

    [Fact]
    public void Read_reads_both_keys()
    {
        var a = TagArrayConfig.Read("""{"FullName":"ns=2;s=X","isArray":true,"arrayLength":8}""");

        a.IsArray.ShouldBeTrue();
        a.ArrayLength.ShouldBe(8u);
    }

    [Fact]
    public void Set_true_with_length_writes_both_camelCase_keys()
    {
        var json = TagArrayConfig.Set("""{"FullName":"ns=2;s=X"}""", isArray: true, arrayLength: 8);

        json.ShouldContain("\"isArray\":true");
        json.ShouldContain("\"arrayLength\":8");
        // The driver field is untouched.
        json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
    }

    [Fact]
    public void Set_true_round_trips_through_Read()
    {
        var json = TagArrayConfig.Set("{}", isArray: true, arrayLength: 16);

        var a = TagArrayConfig.Read(json);
        a.IsArray.ShouldBeTrue();
        a.ArrayLength.ShouldBe(16u);
    }

    [Fact]
    public void Set_false_removes_both_keys()
    {
        var json = TagArrayConfig.Set(
            """{"FullName":"ns=2;s=X","isArray":true,"arrayLength":8}""",
            isArray: false, arrayLength: 8);

        json.ShouldNotContain("isArray");
        json.ShouldNotContain("arrayLength");
        json.ShouldContain("\"FullName\":\"ns=2;s=X\"");
    }

    [Fact]
    public void Set_false_removes_orphan_arrayLength()
    {
        // Clearing isArray must not leave an orphan arrayLength behind.
        var json = TagArrayConfig.Set(
            """{"isArray":true,"arrayLength":4}""",
            isArray: false, arrayLength: 4);

        json.ShouldNotContain("arrayLength");
        json.ShouldNotContain("isArray");
    }

    [Fact]
    public void Set_true_null_length_drops_arrayLength_key()
    {
        // isArray on but no length yet (pre-validation authoring state): emit isArray, no arrayLength.
        var json = TagArrayConfig.Set("{}", isArray: true, arrayLength: null);

        json.ShouldContain("\"isArray\":true");
        json.ShouldNotContain("arrayLength");
    }

    [Fact]
    public void Set_preserves_driver_specific_unknown_keys()
    {
        var json = TagArrayConfig.Set(
            """{"register":40001,"scale":0.1,"nested":{"a":1}}""",
            isArray: true, arrayLength: 32);

        json.ShouldContain("register");
        json.ShouldContain("40001");
        json.ShouldContain("scale");
        json.ShouldContain("0.1");
        json.ShouldContain("nested");
        json.ShouldContain("\"isArray\":true");
        json.ShouldContain("\"arrayLength\":32");
    }

    [Fact]
    public void Set_on_malformed_input_starts_from_empty_object()
    {
        var json = TagArrayConfig.Set("not json", isArray: true, arrayLength: 2);

        json.ShouldContain("\"isArray\":true");
        json.ShouldContain("\"arrayLength\":2");
        json.ShouldStartWith("{");
        json.ShouldEndWith("}");
    }

    [Fact]
    public void Validate_rejects_array_with_null_length()
    {
        TagArrayConfig.Validate(isArray: true, arrayLength: null).ShouldNotBeNull();
    }

    [Fact]
    public void Validate_rejects_array_with_zero_length()
    {
        TagArrayConfig.Validate(isArray: true, arrayLength: 0).ShouldNotBeNull();
    }

    [Fact]
    public void Validate_accepts_array_with_positive_length()
    {
        TagArrayConfig.Validate(isArray: true, arrayLength: 8).ShouldBeNull();
    }

    [Fact]
    public void Validate_accepts_non_array_regardless_of_length()
    {
        TagArrayConfig.Validate(isArray: false, arrayLength: null).ShouldBeNull();
        TagArrayConfig.Validate(isArray: false, arrayLength: 0).ShouldBeNull();
    }

    /// <summary>
    /// Full seam: the array helper and a driver-typed editor compose over the same canonical TagConfig
    /// blob without clobbering each other. Setting the array keys, round-tripping through the OpcUaClient
    /// typed editor (FromJson → ToJson), then reading the array keys back recovers them intact — and the
    /// editor's own NodeId field survives the array merge.
    /// </summary>
    [Fact]
    public void Array_keys_survive_a_typed_editor_round_trip()
    {
        var withArray = TagArrayConfig.Set("""{"nodeId":"ns=2;s=X"}""", isArray: true, arrayLength: 12);

        var afterEditor = OpcUaClientTagConfigModel.FromJson(withArray).ToJson();

        var a = TagArrayConfig.Read(afterEditor);
        a.IsArray.ShouldBeTrue();
        a.ArrayLength.ShouldBe(12u);
        OpcUaClientTagConfigModel.FromJson(afterEditor).NodeId.ShouldBe("ns=2;s=X");
    }

    /// <summary>The array helper and the historize helper compose over the same blob (both preserve
    /// unknown keys), so a tag can carry both array and history intent simultaneously.</summary>
    [Fact]
    public void Array_and_historize_keys_compose()
    {
        var withHistory = TagHistorizeConfig.Set("""{"FullName":"ns=2;s=X"}""", isHistorized: true, historianTagname: "TN");
        var withBoth = TagArrayConfig.Set(withHistory, isArray: true, arrayLength: 4);

        TagArrayConfig.Read(withBoth).ArrayLength.ShouldBe(4u);
        TagHistorizeConfig.Read(withBoth).IsHistorized.ShouldBeTrue();
        TagHistorizeConfig.Read(withBoth).HistorianTagname.ShouldBe("TN");
    }
}
