using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Types;

namespace ZB.MOM.WW.OtOpcUa.Commons.Tests;

/// <summary>
///     The single byte-parity authority for the cross-driver TagConfig platform intents (R2-11, 01/C-1).
///     Ports the three retired <c>OpcUaServer.Tests/ExtractTag{Alarm,Historize,Array}Tests</c> tables plus
///     the never-throws property. Under the v3 identity contract <c>TagConfigIntent</c> no longer derives
///     any FullName/identity — only the platform intents (alarm / historize / array) are parsed here.
/// </summary>
public sealed class TagConfigIntentTests
{
    // ---- Parse(null)/blank defaults (identity no longer carried) ----

    [Fact]
    public void Parse_null_yields_all_defaults()
    {
        var intent = TagConfigIntent.Parse(null);
        intent.Alarm.ShouldBeNull();
        intent.IsHistorized.ShouldBeFalse();
        intent.HistorianTagname.ShouldBeNull();
        intent.IsArray.ShouldBeFalse();
        intent.ArrayLength.ShouldBeNull();
    }

    [Theory]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("not json {")]     // malformed
    [InlineData("[1,2]")]          // non-object root
    [InlineData("42")]             // non-object root
    public void Parse_non_object_yields_all_defaults(string cfg)
    {
        var intent = TagConfigIntent.Parse(cfg);
        intent.Alarm.ShouldBeNull();
        intent.IsHistorized.ShouldBeFalse();
        intent.HistorianTagname.ShouldBeNull();
        intent.IsArray.ShouldBeFalse();
        intent.ArrayLength.ShouldBeNull();
    }

    // ---- Alarm (ported from ExtractTagAlarmTests) ----

    [Theory]
    [InlineData("{\"nodeId\":\"X.Y\"}", false, null, 0)]
    [InlineData("{\"nodeId\":\"X.Y\",\"alarm\":{}}", true, "AlarmCondition", 500)]
    [InlineData("{\"nodeId\":\"X.Y\",\"alarm\":{\"alarmType\":\"OffNormalAlarm\",\"severity\":700}}", true, "OffNormalAlarm", 700)]
    [InlineData("not json", false, null, 0)]
    [InlineData("{\"nodeId\":\"X.Y\",\"alarm\":\"oops\"}", false, null, 0)]
    [InlineData("{\"alarm\":{\"severity\":\"high\"}}", true, "AlarmCondition", 500)]  // non-number severity → 500
    [InlineData("{\"alarm\":{\"alarmType\":123}}", true, "AlarmCondition", 500)]      // non-string type → default
    public void Alarm_parses_or_null(string cfg, bool present, string? type, int sev)
    {
        var alarm = TagConfigIntent.Parse(cfg).Alarm;
        if (!present) { alarm.ShouldBeNull(); return; }
        alarm!.AlarmType.ShouldBe(type);
        alarm.Severity.ShouldBe(sev);
    }

    [Theory]
    [InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500}}", null)]
    [InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":true}}", true)]
    [InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":false}}", false)]
    [InlineData("{\"alarm\":{\"alarmType\":\"LimitAlarm\",\"severity\":500,\"historizeToAveva\":\"oops\"}}", null)]
    public void Alarm_historizeToAveva(string cfg, bool? expected)
    {
        TagConfigIntent.Parse(cfg).Alarm!.HistorizeToAveva.ShouldBe(expected);
    }

    // ---- Historize ----

    [Theory]
    [InlineData("{\"nodeId\":\"A\",\"isHistorized\":true}", true, null)]
    [InlineData("{\"nodeId\":\"A\",\"isHistorized\":true,\"historianTagname\":\"P.L.X\"}", true, "P.L.X")]
    [InlineData("{\"nodeId\":\"A\",\"isHistorized\":false,\"historianTagname\":null}", false, null)]
    [InlineData("{\"nodeId\":\"A\",\"isHistorized\":true,\"historianTagname\":\"   \"}", true, null)]  // whitespace → null
    [InlineData("{\"nodeId\":\"A\",\"isHistorized\":\"yes\"}", false, null)]                          // non-bool → false
    [InlineData("{\"nodeId\":\"A\"}", false, null)]
    public void Historize(string cfg, bool expectedHistorized, string? expectedTagname)
    {
        var intent = TagConfigIntent.Parse(cfg);
        intent.IsHistorized.ShouldBe(expectedHistorized);
        intent.HistorianTagname.ShouldBe(expectedTagname);
    }

    // ---- Array (ported from ExtractTagArrayTests) ----

    [Theory]
    [InlineData("{\"nodeId\":\"T.A\",\"isArray\":true,\"arrayLength\":16}", true, (uint)16)]
    [InlineData("{\"nodeId\":\"T.A\",\"isArray\":true}", true, null)]
    [InlineData("{\"nodeId\":\"T.A\"}", false, null)]
    [InlineData("{\"nodeId\":\"T.A\",\"isArray\":false,\"arrayLength\":16}", false, null)]
    [InlineData("{\"nodeId\":\"T.A\",\"isArray\":true,\"arrayLength\":0}", true, (uint)0)]
    [InlineData(null, false, null)]
    [InlineData("", false, null)]
    [InlineData("not json {", false, null)]
    [InlineData("[1,2]", false, null)]
    [InlineData("{\"isArray\":\"yes\"}", false, null)]
    [InlineData("{\"isArray\":true,\"arrayLength\":\"16\"}", true, null)]
    [InlineData("{\"isArray\":true,\"arrayLength\":-1}", true, null)]
    [InlineData("{\"isArray\":true,\"arrayLength\":16.5}", true, null)]
    [InlineData("{\"isArray\":true,\"arrayLength\":4294967296}", true, null)]
    public void Array_parses_or_defaults(string? cfg, bool expectedIsArray, uint? expectedLength)
    {
        var intent = TagConfigIntent.Parse(cfg);
        intent.IsArray.ShouldBe(expectedIsArray);
        intent.ArrayLength.ShouldBe(expectedLength);
    }

    // ---- never-throws property over an adversarial set ----

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("not json {")]
    [InlineData("[1,2,3]")]
    [InlineData("42")]
    [InlineData("\"just a string\"")]
    [InlineData("{\"alarm\":42,\"isArray\":\"x\",\"arrayLength\":true,\"isHistorized\":123}")]
    public void Parse_never_throws(string? cfg)
    {
        Should.NotThrow(() => TagConfigIntent.Parse(cfg));
    }
}
