using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests;

/// <summary>
///     Dedicated unit tests for <see cref="AbCipTagDefinitionFactory.FromTagConfig"/>.
///     Covers all distinct outcome branches: valid scalar, 1-element array, N-element array,
///     degenerate array shapes, non-JSON input, non-object JSON, blank/missing tagPath,
///     the <c>writable</c> field, and the <c>Structure</c> dataType path. Under v3 the definition's
///     identity (<c>Name</c>) is the RawPath the mapper is handed, not the blob, and the mapper does
///     NOT read <c>deviceHostAddress</c> (device placement travels on the RawTagEntry).
/// </summary>
[Trait("Category", "Unit")]
public class AbCipTagDefinitionFactoryTests
{
    private const string Raw = "line1/eq/tag";

    // ── Happy-path scalar ────────────────────────────────────────────────────────────────

    [Fact]
    public void Valid_scalar_round_trip_parses_all_fields()
    {
        var json = """{"deviceHostAddress":"ab://10.0.0.1/1,0","tagPath":"Motor.Speed","dataType":"Real"}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Name.ShouldBe(Raw);
        def.TagPath.ShouldBe("Motor.Speed");
        def.DeviceHostAddress.ShouldBe(""); // v3: mapper never reads deviceHostAddress
        def.DataType.ShouldBe(AbCipDataType.Real);
        def.Writable.ShouldBeTrue();
        def.IsArray.ShouldBeFalse();
        def.ElementCount.ShouldBe(1);
    }

    // ── Array shape ──────────────────────────────────────────────────────────────────────

    [Fact]
    public void One_element_array_isArray_true_arrayLength_1_is_an_array_not_a_scalar()
    {
        var json = """{"tagPath":"Tags[0]","dataType":"DInt","isArray":true,"arrayLength":1}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.IsArray.ShouldBeTrue();
        def.ElementCount.ShouldBe(1);
    }

    [Fact]
    public void N_element_array_isArray_true_arrayLength_N_parses_correctly()
    {
        var json = """{"tagPath":"Buf","dataType":"SInt","isArray":true,"arrayLength":8}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.IsArray.ShouldBeTrue();
        def.ElementCount.ShouldBe(8);
    }

    [Fact]
    public void IsArray_true_arrayLength_0_is_canonical_scalar()
    {
        // Canonical rule: isArray:true AND arrayLength < 1 → scalar.
        var json = """{"tagPath":"PT_101","isArray":true,"arrayLength":0}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.IsArray.ShouldBeFalse();
        def.ElementCount.ShouldBe(1);
    }

    [Fact]
    public void IsArray_true_arrayLength_absent_is_canonical_scalar()
    {
        // Canonical rule: isArray:true but arrayLength absent → scalar.
        var json = """{"tagPath":"PT_101","isArray":true}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.IsArray.ShouldBeFalse();
        def.ElementCount.ShouldBe(1);
    }

    // ── Rejection paths ──────────────────────────────────────────────────────────────────

    [Fact]
    public void Non_JSON_input_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("not json at all", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Non_object_JSON_array_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("""["tagPath","foo"]""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Non_object_JSON_string_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("\"Motor.Speed\"", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Missing_tagPath_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"dataType":"DInt"}""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void Blank_tagPath_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":"   "}""", Raw, out _).ShouldBeFalse();

    [Fact]
    public void TagPath_as_number_returns_false()
        => AbCipTagDefinitionFactory.FromTagConfig("""{"tagPath":42}""", Raw, out _).ShouldBeFalse();

    // ── Writable field ────────────────────────────────────────────────────────────────────

    [Fact]
    public void Writable_false_is_honoured()
    {
        var json = """{"tagPath":"Sensor.Val","writable":false}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Writable.ShouldBeFalse();
    }

    [Fact]
    public void Writable_absent_defaults_to_true()
    {
        var json = """{"tagPath":"Sensor.Val"}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Writable.ShouldBeTrue();
    }

    [Fact]
    public void Writable_true_explicit_is_honoured()
    {
        var json = """{"tagPath":"Sensor.Val","writable":true}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.Writable.ShouldBeTrue();
    }

    // ── Structure dataType ────────────────────────────────────────────────────────────────

    /// <summary>
    ///     A "dataType":"Structure" equipment-tag input is accepted and produces a Structure-typed
    ///     definition with Members:null. The driver treats the tag path as a black-box dotted-path
    ///     read (libplctag resolves the full path); UDT member declarations are not authored via the
    ///     production equipment-tag flow (AbCipTagConfigModel emits no members key).
    /// </summary>
    [Fact]
    public void Structure_dataType_is_accepted_with_null_Members_and_returns_true()
    {
        var json = """{"tagPath":"Motor","dataType":"Structure"}""";
        AbCipTagDefinitionFactory.FromTagConfig(json, Raw, out var def).ShouldBeTrue();
        def!.DataType.ShouldBe(AbCipDataType.Structure);
        def.Members.ShouldBeNull();
        def.TagPath.ShouldBe("Motor");
    }

    // ── ToTagConfig / FromTagConfig round-trip (v3 RawTagEntry carrier) ────────────────────

    /// <summary>
    ///     A typed definition serialised via <see cref="AbCipTagDefinitionFactory.ToTagConfig"/> and
    ///     re-read via <see cref="AbCipTagDefinitionFactory.FromTagConfig"/> reproduces every per-tag
    ///     field except the out-of-band device routing key + WriteIdempotent (which travel on the
    ///     RawTagEntry, not the blob). Covers the array + declared-member round-trip the driver-test
    ///     AbCipRawTags helper relies on.
    /// </summary>
    [Fact]
    public void ToTagConfig_FromTagConfig_round_trips_arrays_and_members()
    {
        var def = new AbCipTagDefinition(
            Name: "Motor1", DeviceHostAddress: "ab://10.0.0.5/1,0", TagPath: "Motor1",
            DataType: AbCipDataType.Structure, Writable: false, SafetyTag: true,
            Members:
            [
                new AbCipStructureMember("Speed", AbCipDataType.DInt),
                new AbCipStructureMember("Buf", AbCipDataType.Real, Writable: false, ElementCount: 4, IsArray: true),
            ]);

        var blob = AbCipTagDefinitionFactory.ToTagConfig(def);
        AbCipTagDefinitionFactory.FromTagConfig(blob, "Motor1", out var back).ShouldBeTrue();

        back!.Name.ShouldBe("Motor1");
        back.DeviceHostAddress.ShouldBe(""); // routing key not in the blob
        back.TagPath.ShouldBe("Motor1");
        back.DataType.ShouldBe(AbCipDataType.Structure);
        back.Writable.ShouldBeFalse();
        back.SafetyTag.ShouldBeTrue();
        back.Members!.Count.ShouldBe(2);
        back.Members[0].Name.ShouldBe("Speed");
        back.Members[1].IsArray.ShouldBeTrue();
        back.Members[1].ElementCount.ShouldBe(4);
        back.Members[1].Writable.ShouldBeFalse();
    }
}
