using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Threading;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;

namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;

/// <summary>
///     Unit-test coverage for <see cref="WnWrapAlarmConsumer"/>'s pure
///     parsing helpers — XML payload → <see cref="MxAlarmSnapshotRecord"/>
///     dictionary, and the 32-char-hex GUID round-trip. The COM-side
///     polling loop is verified separately by the Skip-gated
///     <c>WnWrapConsumerProbeTests</c> on a live AVEVA install.
/// </summary>
public sealed class WnWrapAlarmConsumerXmlTests
{
    /// <summary>Captured XML from the dev rig (probe run 2026-05-01).</summary>
    private const string SingleAlarmActiveXml =
        "<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"1\">" +
        "<ALARM><GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>" +
        "<DATE>2026/5/1</DATE><TIME>13:26:14.709</TIME>" +
        "<GMTOFFSET>240</GMTOFFSET><DSTADJUST>0</DSTADJUST>" +
        "<PROVIDER_NODE>DESKTOP-6JL3KKO</PROVIDER_NODE>" +
        "<PROVIDER_NAME>Galaxy</PROVIDER_NAME>" +
        "<GROUP>TestArea</GROUP>" +
        "<TAGNAME>TestMachine_001.TestAlarm001</TAGNAME>" +
        "<TYPE>DSC</TYPE><VALUE>true</VALUE><LIMIT>true</LIMIT>" +
        "<PRIORITY>500</PRIORITY><STATE>UNACK_ALM</STATE>" +
        "<OPERATOR_NODE></OPERATOR_NODE><OPERATOR_NAME></OPERATOR_NAME>" +
        "<ALARM_COMMENT>Test alarm #1</ALARM_COMMENT></ALARM>" +
        "</ALARM_RECORDS>";

    private const string EmptyXml =
        "<?xml version=\"1.0\"?><ALARM_RECORDS COUNT=\"0\"></ALARM_RECORDS>";

    [Fact]
    public void ParseSnapshotXml_WithEmptyPayload_ReturnsEmptyDictionary()
    {
        var records = WnWrapAlarmConsumer.ParseSnapshotXml(EmptyXml);
        Assert.Empty(records);
    }

    [Fact]
    public void ParseSnapshotXml_WithNullOrWhitespace_ReturnsEmptyDictionary()
    {
        Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(""));
        Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml("   "));
    }

    [Fact]
    public void ParseSnapshotXml_WithSingleActiveAlarm_DecodesRecord()
    {
        var records = WnWrapAlarmConsumer.ParseSnapshotXml(SingleAlarmActiveXml);

        Assert.Single(records);
        Guid expectedGuid = new Guid("BCC47053-9542-4D65-BDAA-BCDEA6A32A73");
        var record = records[expectedGuid];
        Assert.Equal(expectedGuid, record.AlarmGuid);
        Assert.Equal("DESKTOP-6JL3KKO", record.ProviderNode);
        Assert.Equal("Galaxy", record.ProviderName);
        Assert.Equal("TestArea", record.Group);
        Assert.Equal("TestMachine_001.TestAlarm001", record.TagName);
        Assert.Equal("DSC", record.Type);
        Assert.Equal("true", record.Value);
        Assert.Equal("true", record.Limit);
        Assert.Equal(500, record.Priority);
        Assert.Equal(MxAlarmStateKind.UnackAlm, record.State);
        Assert.Equal("Test alarm #1", record.AlarmComment);
        Assert.Equal(DateTimeKind.Utc, record.TransitionTimestampUtc.Kind);
        // 13:26:14.709 EDT (UTC-4, DSTADJUST=0) + 240 minutes = 17:26:14.709 UTC.
        Assert.Equal(17, record.TransitionTimestampUtc.Hour);
        Assert.Equal(26, record.TransitionTimestampUtc.Minute);
    }

    [Fact]
    public void ParseSnapshotXml_WithInvalidGuids_SilentlyDropsRecords()
    {
        string xml = SingleAlarmActiveXml.Replace(
            "<GUID>BCC4705395424D65BDAABCDEA6A32A73</GUID>",
            "<GUID>not-a-guid</GUID>");
        Assert.Empty(WnWrapAlarmConsumer.ParseSnapshotXml(xml));
    }

    [Theory]
    [InlineData("BCC4705395424D65BDAABCDEA6A32A73", "BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
    [InlineData("00000000000000000000000000000000", "00000000-0000-0000-0000-000000000000")]
    public void TryParseHexGuid_WithDashless32CharHex_Parses(string hex, string expected)
    {
        Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid));
        Assert.Equal(new Guid(expected), guid);
    }

    [Theory]
    [InlineData("BCC47053-9542-4D65-BDAA-BCDEA6A32A73")]
    public void TryParseHexGuid_WithCanonicalDashedForm_Accepts(string canonical)
    {
        Assert.True(WnWrapAlarmConsumer.TryParseHexGuid(canonical, out Guid guid));
        Assert.Equal(new Guid(canonical), guid);
    }

    [Theory]
    [InlineData(null)]
    [InlineData("")]
    [InlineData("   ")]
    [InlineData("nope")]
    [InlineData("0123456789ABCDEF")] // too short
    [InlineData("BCC4705395424D65BDAABCDEA6A32A73XX")] // too long
    public void TryParseHexGuid_WithInvalidInput_Rejects(string? hex)
    {
        Assert.False(WnWrapAlarmConsumer.TryParseHexGuid(hex, out Guid guid));
        Assert.Equal(Guid.Empty, guid);
    }

    /// <summary>
    ///     Worker-001 regression: the consumer must own no internal
    ///     <see cref="Timer"/>. A thread-pool timer calling the
    ///     apartment-threaded wnwrap COM object off its owning STA can
    ///     deadlock on cross-apartment marshaling, so the timer field and
    ///     callback must not exist on the type.
    /// </summary>
    [Fact]
    public void WnWrapAlarmConsumer_ByReflection_HasNoInternalTimerField()
    {
        FieldInfo[] fields = typeof(WnWrapAlarmConsumer)
            .GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

        Assert.DoesNotContain(fields, field => field.FieldType == typeof(Timer));
        Assert.Null(typeof(WnWrapAlarmConsumer).GetMethod(
            "OnPoll",
            BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic));
    }

    /// <summary>
    ///     Worker-001 regression: no public constructor may accept a
    ///     poll-interval parameter. A non-zero poll interval was the only
    ///     way to arm the off-STA timer; removing the parameter makes the
    ///     footgun structurally unreachable.
    /// </summary>
    [Fact]
    public void WnWrapAlarmConsumer_ByReflection_ExposesNoPollIntervalConstructorParameter()
    {
        foreach (ConstructorInfo constructor in typeof(WnWrapAlarmConsumer)
            .GetConstructors(BindingFlags.Instance | BindingFlags.Public))
        {
            Assert.DoesNotContain(
                constructor.GetParameters(),
                parameter => parameter.Name is not null
                    && parameter.Name.IndexOf("poll", StringComparison.OrdinalIgnoreCase) >= 0);
        }
    }

    /// <summary>
    ///     Worker.Tests-022: pins the "new alarm sighting" branch of
    ///     <see cref="WnWrapAlarmConsumer.ComputeTransitions"/>. A GUID
    ///     that appears in <c>next</c> but not in <c>previous</c> must
    ///     produce exactly one transition with
    ///     <see cref="MxAlarmStateKind.Unspecified"/> as the previous
    ///     state — the proto layer relies on this sentinel to map a
    ///     first sighting to a <c>Raise</c>.
    /// </summary>
    [Fact]
    public void ComputeTransitions_WhenAlarmIsNewInNextSnapshot_EmitsTransitionWithUnspecifiedPreviousState()
    {
        Guid alarmGuid = new Guid("BCC47053-9542-4D65-BDAA-BCDEA6A32A73");
        Dictionary<Guid, MxAlarmSnapshotRecord> previous = new();
        Dictionary<Guid, MxAlarmSnapshotRecord> next = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.UnackAlm),
        };

        IReadOnlyList<MxAlarmTransitionEvent> transitions =
            WnWrapAlarmConsumer.ComputeTransitions(previous, next);

        MxAlarmTransitionEvent single = Assert.Single(transitions);
        Assert.Equal(alarmGuid, single.Record.AlarmGuid);
        Assert.Equal(MxAlarmStateKind.UnackAlm, single.Record.State);
        Assert.Equal(MxAlarmStateKind.Unspecified, single.PreviousState);
    }

    /// <summary>
    ///     Worker.Tests-022: pins the "state unchanged" branch. A GUID
    ///     present in both snapshots with identical
    ///     <see cref="MxAlarmSnapshotRecord.State"/> must produce no
    ///     transition — a regression that emits a transition every poll
    ///     regardless of state change would slip through without this
    ///     test.
    /// </summary>
    [Fact]
    public void ComputeTransitions_WhenAlarmStateUnchanged_EmitsNoTransition()
    {
        Guid alarmGuid = Guid.NewGuid();
        Dictionary<Guid, MxAlarmSnapshotRecord> previous = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.UnackAlm),
        };
        Dictionary<Guid, MxAlarmSnapshotRecord> next = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.UnackAlm),
        };

        IReadOnlyList<MxAlarmTransitionEvent> transitions =
            WnWrapAlarmConsumer.ComputeTransitions(previous, next);

        Assert.Empty(transitions);
    }

    /// <summary>
    ///     Worker.Tests-022: pins the "state changed" branch. A GUID
    ///     present in both snapshots with a different state must produce
    ///     one transition carrying the prior state so the proto layer
    ///     can distinguish e.g. <c>UnackAlm</c>→<c>AckAlm</c>
    ///     (Acknowledge) from <c>Unspecified</c>→<c>UnackAlm</c> (Raise).
    /// </summary>
    [Fact]
    public void ComputeTransitions_WhenAlarmStateChanged_EmitsTransitionWithPriorState()
    {
        Guid alarmGuid = Guid.NewGuid();
        Dictionary<Guid, MxAlarmSnapshotRecord> previous = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.UnackAlm),
        };
        Dictionary<Guid, MxAlarmSnapshotRecord> next = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.AckAlm),
        };

        IReadOnlyList<MxAlarmTransitionEvent> transitions =
            WnWrapAlarmConsumer.ComputeTransitions(previous, next);

        MxAlarmTransitionEvent single = Assert.Single(transitions);
        Assert.Equal(alarmGuid, single.Record.AlarmGuid);
        Assert.Equal(MxAlarmStateKind.AckAlm, single.Record.State);
        Assert.Equal(MxAlarmStateKind.UnackAlm, single.PreviousState);
    }

    /// <summary>
    ///     Worker.Tests-022: pins the "alarm cleared from the active set"
    ///     branch. AVEVA drops cleared alarms from
    ///     <c>GetXmlCurrentAlarms2</c>'s active set rather than emitting a
    ///     transition record. A GUID present in
    ///     <c>previous</c> but absent from <c>next</c> must therefore
    ///     produce no transition; the diff treats disappearance as an
    ///     implicit clear that the proto layer recognises by the missing
    ///     GUID, not by an emitted event.
    /// </summary>
    [Fact]
    public void ComputeTransitions_WhenAlarmDroppedFromActiveSet_EmitsNoTransition()
    {
        Guid alarmGuid = Guid.NewGuid();
        Dictionary<Guid, MxAlarmSnapshotRecord> previous = new()
        {
            [alarmGuid] = NewRecord(alarmGuid, MxAlarmStateKind.UnackAlm),
        };
        Dictionary<Guid, MxAlarmSnapshotRecord> next = new();

        IReadOnlyList<MxAlarmTransitionEvent> transitions =
            WnWrapAlarmConsumer.ComputeTransitions(previous, next);

        Assert.Empty(transitions);
    }

    /// <summary>
    ///     Worker.Tests-022: pins the multi-alarm fan-out. Multiple
    ///     simultaneous transitions (new + changed + unchanged + dropped)
    ///     in one snapshot must produce exactly the changed and new
    ///     entries — not the unchanged and not the dropped.
    /// </summary>
    [Fact]
    public void ComputeTransitions_WithMixedDelta_EmitsOnlyNewAndChangedTransitions()
    {
        Guid newGuid = Guid.NewGuid();
        Guid changedGuid = Guid.NewGuid();
        Guid unchangedGuid = Guid.NewGuid();
        Guid droppedGuid = Guid.NewGuid();

        Dictionary<Guid, MxAlarmSnapshotRecord> previous = new()
        {
            [changedGuid] = NewRecord(changedGuid, MxAlarmStateKind.UnackAlm),
            [unchangedGuid] = NewRecord(unchangedGuid, MxAlarmStateKind.AckAlm),
            [droppedGuid] = NewRecord(droppedGuid, MxAlarmStateKind.UnackAlm),
        };
        Dictionary<Guid, MxAlarmSnapshotRecord> next = new()
        {
            [newGuid] = NewRecord(newGuid, MxAlarmStateKind.UnackAlm),
            [changedGuid] = NewRecord(changedGuid, MxAlarmStateKind.AckAlm),
            [unchangedGuid] = NewRecord(unchangedGuid, MxAlarmStateKind.AckAlm),
        };

        IReadOnlyList<MxAlarmTransitionEvent> transitions =
            WnWrapAlarmConsumer.ComputeTransitions(previous, next);

        Assert.Equal(2, transitions.Count);

        MxAlarmTransitionEvent newTransition = Assert.Single(
            transitions,
            t => t.Record.AlarmGuid == newGuid);
        Assert.Equal(MxAlarmStateKind.Unspecified, newTransition.PreviousState);
        Assert.Equal(MxAlarmStateKind.UnackAlm, newTransition.Record.State);

        MxAlarmTransitionEvent changedTransition = Assert.Single(
            transitions,
            t => t.Record.AlarmGuid == changedGuid);
        Assert.Equal(MxAlarmStateKind.UnackAlm, changedTransition.PreviousState);
        Assert.Equal(MxAlarmStateKind.AckAlm, changedTransition.Record.State);
    }

    private static MxAlarmSnapshotRecord NewRecord(Guid guid, MxAlarmStateKind state)
    {
        return new MxAlarmSnapshotRecord
        {
            AlarmGuid = guid,
            State = state,
            TagName = "TestMachine.TestAlarm",
            ProviderNode = "TEST-NODE",
            ProviderName = "Galaxy",
        };
    }
}
