using System;
using System.Collections.Generic;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
using Xunit;

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

/// <summary>
///     Unit tests for <see cref="SubtagAlarmConsumer"/>: prove that the subtag
///     fallback advises the observable subtags, synthesizes degraded
///     transitions with stable synthetic GUIDs, routes acknowledgments to the
///     ack-comment subtag, and stamps snapshots. A <see cref="FakeSource"/>
///     stands in for the live MXAccess subtag source so this needs no AVEVA
///     install.
/// </summary>
public sealed class SubtagAlarmConsumerTests
{
    private const string Reference = "Galaxy!TestArea.Tank01.Level.HiHi";
    private const string ActiveSubtag = "Tank01.Level.HiHi.InAlarm";
    private const string AckedSubtag = "Tank01.Level.HiHi.Acked";
    private const string AckCommentSubtag = "Tank01.Level.HiHi.AckComment";
    private const string PrioritySubtag = "Tank01.Level.HiHi.Priority";

    private static AlarmSubtagTarget BuildTarget()
    {
        return new AlarmSubtagTarget
        {
            AlarmFullReference = Reference,
            SourceObjectReference = "Tank01",
            ActiveSubtag = ActiveSubtag,
            AckedSubtag = AckedSubtag,
            AckCommentSubtag = AckCommentSubtag,
            PrioritySubtag = PrioritySubtag,
        };
    }

    private static SubtagAlarmConsumer BuildConsumer(FakeSource source)
    {
        return new SubtagAlarmConsumer(source, new[] { BuildTarget() });
    }

    /// <summary>Verifies Subscribe advises the active, acked, priority, and ack-comment subtags (the ack-comment is advised so it is an active, writable MXAccess item).</summary>
    [Fact]
    public void Subscribe_AdvisesAllSubtagsIncludingAckComment()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);

        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        Assert.Contains(ActiveSubtag, source.Advised);
        Assert.Contains(AckedSubtag, source.Advised);
        Assert.Contains(PrioritySubtag, source.Advised);
        Assert.Contains(AckCommentSubtag, source.Advised);
    }

    /// <summary>Verifies an active=true value change raises a degraded, GUID-stamped UNACK_ALM transition.</summary>
    [Fact]
    public void ValueChange_RaisesDegradedSynthesizedTransition()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        MxAlarmTransitionEvent? emitted = null;
        consumer.AlarmTransitionEmitted += (_, e) => emitted = e;

        source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));

        Assert.NotNull(emitted);
        Assert.Equal(MxAlarmStateKind.UnackAlm, emitted!.Record.State);
        Assert.True(emitted.Record.Degraded);
        Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
    }

    /// <summary>Verifies AcknowledgeByName writes the comment to the ack-comment subtag and returns success.</summary>
    [Fact]
    public void AcknowledgeByName_WritesCommentToAckCommentSubtag()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        int rc = consumer.AcknowledgeByName(
            alarmName: "Tank01.Level.HiHi",
            providerName: "Galaxy",
            groupName: "TestArea",
            ackComment: "operator ack",
            ackOperatorName: "alice",
            ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP",
            ackOperatorFullName: "Alice Smith");

        Assert.Equal(0, rc);
        Assert.NotNull(source.LastWrite);
        Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
        Assert.Equal("operator ack", source.LastWrite!.Value.Value);
    }

    /// <summary>Verifies AcknowledgeByName returns non-zero when no target matches the supplied name.</summary>
    [Fact]
    public void AcknowledgeByName_UnknownAlarm_ReturnsNonZero()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        int rc = consumer.AcknowledgeByName(
            alarmName: "DoesNotExist.NoSuchAlarm",
            providerName: "Galaxy",
            groupName: "TestArea",
            ackComment: "operator ack",
            ackOperatorName: "alice",
            ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP",
            ackOperatorFullName: "Alice Smith");

        Assert.NotEqual(0, rc);
        Assert.Null(source.LastWrite);
    }

    /// <summary>Verifies a snapshot of an active alarm stamps Degraded and a non-empty synthetic GUID.</summary>
    [Fact]
    public void SnapshotActiveAlarms_StampsDegradedAndGuid()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));

        IReadOnlyList<MxAlarmSnapshotRecord> snapshot = consumer.SnapshotActiveAlarms(out _);

        Assert.Single(snapshot);
        Assert.True(snapshot[0].Degraded);
        Assert.NotEqual(Guid.Empty, snapshot[0].AlarmGuid);
    }

    /// <summary>Verifies the synthetic GUID on the emitted transition equals the GUID in the snapshot for the same alarm.</summary>
    [Fact]
    public void SameReference_SyntheticGuidStableAcrossTransitionAndSnapshot()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        MxAlarmTransitionEvent? emitted = null;
        consumer.AlarmTransitionEmitted += (_, e) => emitted = e;

        source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));

        IReadOnlyList<MxAlarmSnapshotRecord> snapshot = consumer.SnapshotActiveAlarms(out _);

        Assert.NotNull(emitted);
        Assert.Single(snapshot);
        Assert.Equal(emitted!.Record.AlarmGuid, snapshot[0].AlarmGuid);
        Assert.NotEqual(Guid.Empty, emitted.Record.AlarmGuid);
    }

    /// <summary>
    ///     Verifies that when two alarm targets share a prefix (e.g. Level.Hi vs Level.HiHi),
    ///     AcknowledgeByName routes each ack to its own ack-comment subtag and never
    ///     conflates the shorter name with the longer one.
    /// </summary>
    [Fact]
    public void AcknowledgeByName_PrefixNameDoesNotFalseMatch()
    {
        const string ReferenceHi = "Galaxy!Area.Tank01.Level.Hi";
        const string ReferenceHiHi = "Galaxy!Area.Tank01.Level.HiHi";
        const string AckCommentHi = "Tank01.Level.Hi.AckComment";
        const string AckCommentHiHi = "Tank01.Level.HiHi.AckComment";

        AlarmSubtagTarget targetHi = new AlarmSubtagTarget
        {
            AlarmFullReference = ReferenceHi,
            SourceObjectReference = "Tank01",
            ActiveSubtag = "Tank01.Level.Hi.InAlarm",
            AckedSubtag = "Tank01.Level.Hi.Acked",
            AckCommentSubtag = AckCommentHi,
            PrioritySubtag = "Tank01.Level.Hi.Priority",
        };
        AlarmSubtagTarget targetHiHi = new AlarmSubtagTarget
        {
            AlarmFullReference = ReferenceHiHi,
            SourceObjectReference = "Tank01",
            ActiveSubtag = "Tank01.Level.HiHi.InAlarm",
            AckedSubtag = "Tank01.Level.HiHi.Acked",
            AckCommentSubtag = AckCommentHiHi,
            PrioritySubtag = "Tank01.Level.HiHi.Priority",
        };

        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(
            source, new[] { targetHi, targetHiHi });
        consumer.Subscribe(@"\\HOST\Galaxy!Area");

        // Ack the shorter name — must write to the shorter target's subtag only.
        int rcHi = consumer.AcknowledgeByName(
            alarmName: "Tank01.Level.Hi",
            providerName: "Galaxy",
            groupName: "Area",
            ackComment: "ack hi",
            ackOperatorName: "op", ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");

        Assert.Equal(0, rcHi);
        Assert.NotNull(source.LastWrite);
        Assert.Equal(AckCommentHi, source.LastWrite!.Value.Address);
        Assert.Equal("ack hi", source.LastWrite.Value.Value);

        // Ack the longer name — must write to the longer target's subtag.
        int rcHiHi = consumer.AcknowledgeByName(
            alarmName: "Tank01.Level.HiHi",
            providerName: "Galaxy",
            groupName: "Area",
            ackComment: "ack hihi",
            ackOperatorName: "op", ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP", ackOperatorFullName: "Operator");

        Assert.Equal(0, rcHiHi);
        Assert.NotNull(source.LastWrite);
        Assert.Equal(AckCommentHiHi, source.LastWrite!.Value.Address);
        Assert.Equal("ack hihi", source.LastWrite.Value.Value);
    }

    /// <summary>
    ///     Verifies AcknowledgeByGuid resolves the synthetic GUID (computed from
    ///     the alarm's full reference) to the correct target and writes the comment
    ///     to that target's ack-comment subtag.
    /// </summary>
    [Fact]
    public void AcknowledgeByGuid_WritesCommentToAckCommentSubtag()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        // Raise a transition so the state machine sees the alarm, then capture
        // the GUID stamped on the emitted event.
        MxAlarmTransitionEvent? emitted = null;
        consumer.AlarmTransitionEmitted += (_, e) => emitted = e;
        source.Raise(ActiveSubtag, true, new DateTime(2026, 6, 13, 10, 0, 0, DateTimeKind.Utc));

        Assert.NotNull(emitted);
        Guid syntheticGuid = emitted!.Record.AlarmGuid;
        Assert.NotEqual(Guid.Empty, syntheticGuid);

        int rc = consumer.AcknowledgeByGuid(
            alarmGuid: syntheticGuid,
            ackComment: "guid ack",
            ackOperatorName: "op",
            ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP",
            ackOperatorFullName: "Operator");

        Assert.Equal(0, rc);
        Assert.NotNull(source.LastWrite);
        Assert.Equal(AckCommentSubtag, source.LastWrite!.Value.Address);
        Assert.Equal("guid ack", source.LastWrite.Value.Value);
    }

    /// <summary>
    ///     Verifies AcknowledgeByGuid returns non-zero and performs no write when
    ///     the supplied GUID is not known to the consumer.
    /// </summary>
    [Fact]
    public void AcknowledgeByGuid_UnknownGuid_ReturnsNonZero()
    {
        FakeSource source = new FakeSource();
        using SubtagAlarmConsumer consumer = BuildConsumer(source);
        consumer.Subscribe(@"\\HOST\Galaxy!TestArea");

        int rc = consumer.AcknowledgeByGuid(
            alarmGuid: Guid.NewGuid(),
            ackComment: "should not write",
            ackOperatorName: "op",
            ackOperatorNode: "WS01",
            ackOperatorDomain: "CORP",
            ackOperatorFullName: "Operator");

        Assert.NotEqual(0, rc);
        Assert.Null(source.LastWrite);
    }

    private sealed class FakeSource : ISubtagAlarmSource
    {
        /// <summary>Raised when an advised subtag reports a new value.</summary>
        public event EventHandler<SubtagValueChange>? ValueChanged;

        /// <summary>Gets the subtag addresses passed to <see cref="Advise"/>.</summary>
        public List<string> Advised { get; } = new List<string>();

        /// <summary>Gets the most recent (address, value) pair passed to <see cref="Write"/>.</summary>
        public (string Address, object? Value)? LastWrite { get; private set; }

        /// <inheritdoc />
        public void Advise(IReadOnlyCollection<string> itemAddresses)
        {
            Advised.AddRange(itemAddresses);
        }

        /// <inheritdoc />
        public void Write(string itemAddress, object? value)
        {
            LastWrite = (itemAddress, value);
        }

        /// <summary>Raises a <see cref="SubtagValueChange"/> for the given subtag.</summary>
        /// <param name="address">The subtag address whose value changed.</param>
        /// <param name="value">The new value.</param>
        /// <param name="timestampUtc">The UTC timestamp of the change.</param>
        public void Raise(string address, object? value, DateTime timestampUtc)
        {
            ValueChanged?.Invoke(this, new SubtagValueChange
            {
                ItemAddress = address,
                Value = value,
                TimestampUtc = timestampUtc,
            });
        }

        /// <inheritdoc />
        public void Dispose()
        {
        }
    }
}
