using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
using Xunit.Abstractions;

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

/// <summary>
///     Live dev-rig smoke test for the alarms-over-gateway pipeline.
///     Exercises <see cref="WnWrapAlarmConsumer"/> + <see cref="AlarmDispatcher"/> +
///     <see cref="MxAccessAlarmEventSink"/> end-to-end against the actual
///     AVEVA System Platform install: subscribes to
///     <c>\\&lt;machine&gt;\Galaxy!DEV</c>, waits for at least one alarm
///     transition (the dev rig's flip script writes
///     <c>TestMachine_001.TestAlarm001</c> every 10s), drains the proto
///     <c>OnAlarmTransitionEvent</c> from the queue, then ack-by-name's
///     it and verifies the ack registers as a subsequent
///     <see cref="AlarmTransitionKind.Acknowledge"/> transition.
///
///     Skip-gated; flip <c>Skip=null</c> on the dev rig with the flip
///     script running.
/// </summary>
public sealed class AlarmsLiveSmokeTests
{
    private static readonly string SubscriptionExpression =
        $@"\\{Environment.MachineName}\Galaxy!DEV";
    private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(45);
    private static readonly TimeSpan TransitionWaitTimeout = TimeSpan.FromSeconds(20);

    private const string SessionId = "alarms-live-smoke";

    private readonly ITestOutputHelper output;
    private readonly Stopwatch elapsed = Stopwatch.StartNew();
    private readonly ConcurrentQueue<string> log = new ConcurrentQueue<string>();

    /// <summary>Initializes a new instance of the AlarmsLiveSmokeTests class.</summary>
    /// <param name="output">Test output helper for logging.</param>
    public AlarmsLiveSmokeTests(ITestOutputHelper output)
    {
        this.output = output;
    }

    /// <summary>Verifies the alarm pipeline raises and acknowledges alarms correctly.</summary>
    [Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + the alarm flip script running. Verified working 2026-05-01.")]
    public void Alarms_FullPipelineRoundTrip_RaisesAndAcknowledges()
    {
        Exception? threadException = null;
        var done = new ManualResetEventSlim(false);
        var thread = new Thread(() =>
        {
            try { RunSmoke(); }
            catch (Exception ex) { threadException = ex; }
            finally { done.Set(); }
        });
        thread.IsBackground = false;
        thread.SetApartmentState(ApartmentState.STA);
        thread.Start();
        done.Wait();
        thread.Join();

        output.WriteLine($"Captured {log.Count} log line(s):");
        while (log.TryDequeue(out string? line))
        {
            output.WriteLine(line);
        }

        if (threadException != null)
        {
            throw threadException;
        }
    }

    private void RunSmoke()
    {
        Log($"Subscription expression: {SubscriptionExpression}");
        Log($"Pump duration: {PumpDuration.TotalSeconds:F0}s; transition wait timeout: {TransitionWaitTimeout.TotalSeconds:F0}s");

        MxAccessEventQueue queue = new MxAccessEventQueue();
        // The consumer owns no internal timer; we drive PollOnce manually
        // from the STA below (the wnwrap COM is ThreadingModel=Apartment,
        // and this test doesn't run a Win32 message pump on its STA).
        WnWrapAlarmConsumer consumer = new WnWrapAlarmConsumer(
            new WNWRAPCONSUMERLib.wwAlarmConsumerClass(),
            maxAlarmsPerFetch: 1024);
        MxAccessAlarmEventSink sink = new MxAccessAlarmEventSink(queue, new MxAccessEventMapper());
        using AlarmDispatcher dispatcher = new AlarmDispatcher(consumer, sink, SessionId);

        Log("Constructed consumer + sink + dispatcher.");
        dispatcher.Subscribe(SubscriptionExpression);
        Log("Subscribe -> ok. Driving PollOnce manually from this STA...");

        // The wnwrap COM object is ThreadingModel=Apartment. The consumer
        // owns no internal timer, so we drive PollOnce manually here on the
        // STA. Production hosting routes polls through the worker's
        // StaRuntime.

        // 1. Wait for the first transition (any kind), then keep waiting
        //    for one with kind=Raise so the alarm is currently Active when
        //    we try to ack. AVEVA rejects acks of cleared alarms with -55,
        //    so we have to time the ack against the flip script's 10s
        //    cadence.
        OnAlarmTransitionEvent? raiseBody = null;
        DateTime raiseDeadline = DateTime.UtcNow + TimeSpan.FromSeconds(30);
        while (DateTime.UtcNow < raiseDeadline && raiseBody is null)
        {
            WorkerEvent? evt = WaitForTransition(queue, TransitionWaitTimeout, "raise", consumer);
            if (evt is null) break;
            OnAlarmTransitionEvent body = evt.Event.OnAlarmTransition;
            Log("Transition: " + DescribeTransition(body));
            Assert.Equal(SessionId, evt.Event.SessionId);
            if (body.TransitionKind == AlarmTransitionKind.Raise)
            {
                raiseBody = body;
            }
        }
        Assert.NotNull(raiseBody);
        Assert.False(string.IsNullOrEmpty(raiseBody!.AlarmFullReference));
        Assert.Contains("Galaxy", raiseBody.AlarmFullReference);

        // 2. Snapshot the active set + verify the captured alarm is there.
        var snapshot = dispatcher.SnapshotActiveAlarms(out _);
        Log($"SnapshotActiveAlarms count={snapshot.Count}");
        foreach (var s in snapshot)
        {
            Log("  active: " + DescribeSnapshot(s));
        }
        Assert.NotEmpty(snapshot);
        Assert.Contains(snapshot, s => s.AlarmFullReference == raiseBody.AlarmFullReference);

        // 3. Ack-by-name using the captured reference. Parse the reference
        //    via the same convention the gateway dispatcher uses
        //    (Provider!Group.Tag where the tag may contain dots).
        Assert.True(TryParseReference(
            raiseBody.AlarmFullReference,
            out string provider, out string group, out string alarmName),
            $"Captured reference '{raiseBody.AlarmFullReference}' did not parse as Provider!Group.Tag.");
        Log($"Ack target: provider='{provider}' group='{group}' name='{alarmName}'");

        // Try the ack with real Windows identity. AVEVA's AlarmAckByName
        // may reject synthetic operator strings; using the current process
        // identity gives the alarm-history a recognizable principal.
        string realUser = Environment.UserName;
        string realNode = Environment.MachineName;
        string realDomain = Environment.UserDomainName ?? string.Empty;
        Log($"Ack identity: user='{realUser}' node='{realNode}' domain='{realDomain}'");

        int rc = dispatcher.AcknowledgeByName(
            alarmName: alarmName,
            providerName: provider,
            groupName: group,
            ackComment: "alarms-live-smoke ack",
            ackOperatorName: realUser,
            ackOperatorNode: realNode,
            ackOperatorDomain: realDomain,
            ackOperatorFullName: realUser);
        Log($"AcknowledgeByName(real identity) -> rc={rc}");

        Assert.Equal(0, rc);

        // 4. Wait for the post-ack transition. With the alarm flipping every
        //    10s and the consumer polling every 500ms, the next state
        //    change should be either kind=Acknowledge (the ack we just
        //    sent registered as a state delta UnackAlm → AckAlm) or the
        //    flip script's next Clear (UnackAlm → UnackRtn).
        WorkerEvent? second = WaitForTransition(queue, TransitionWaitTimeout, "post-ack", consumer);
        Assert.NotNull(second);
        OnAlarmTransitionEvent secondBody = second!.Event.OnAlarmTransition;
        Log("Post-ack transition: " + DescribeTransition(secondBody));
        Assert.NotEqual(AlarmTransitionKind.Unspecified, secondBody.TransitionKind);

        // 5. Pump a little longer to confirm the consumer keeps reporting
        //    transitions on the 10s flip cadence.
        DateTime deadline = DateTime.UtcNow + PumpDuration;
        int additional = 0;
        while (DateTime.UtcNow < deadline)
        {
            consumer.PollOnce();
            if (queue.TryDequeue(out WorkerEvent? evt) && evt is not null)
            {
                additional++;
                OnAlarmTransitionEvent body = evt.Event.OnAlarmTransition;
                Log($"  +{additional}: " + DescribeTransition(body));
            }
            Thread.Sleep(500);
        }
        Log($"Pump completed; additional transitions captured: {additional}.");
    }

    private WorkerEvent? WaitForTransition(
        MxAccessEventQueue queue,
        TimeSpan timeout,
        string label,
        WnWrapAlarmConsumer consumer)
    {
        DateTime deadline = DateTime.UtcNow + timeout;
        int pollCount = 0;
        while (DateTime.UtcNow < deadline)
        {
            try
            {
                consumer.PollOnce();
                pollCount++;
                if (pollCount == 1) Log("First PollOnce returned without throw.");
            }
            catch (Exception ex)
            {
                Log($"PollOnce threw on poll #{pollCount + 1}: {ex.GetType().Name}: {ex.Message}");
                if (ex is System.Runtime.InteropServices.COMException ce)
                {
                    Log($"  HResult=0x{(uint)ce.HResult:X8}");
                }
                throw;
            }
            if (queue.TryDequeue(out WorkerEvent? evt) && evt is not null)
            {
                if (evt.Event.Family == MxEventFamily.OnAlarmTransition)
                {
                    return evt;
                }
                Log($"Skipped non-alarm event (family={evt.Event.Family}) while waiting for {label}.");
            }
            Thread.Sleep(500);
        }
        Log($"Timed out waiting for {label} transition after {timeout.TotalSeconds:F0}s (poll count={pollCount}).");
        return null;
    }

    private static bool TryParseReference(
        string reference,
        out string provider,
        out string group,
        out string alarmName)
    {
        provider = group = alarmName = string.Empty;
        if (string.IsNullOrWhiteSpace(reference)) return false;
        int bang = reference.IndexOf('!');
        if (bang <= 0 || bang == reference.Length - 1) return false;
        string left = reference.Substring(0, bang);
        string right = reference.Substring(bang + 1);
        int dot = right.IndexOf('.');
        if (dot <= 0 || dot == right.Length - 1) return false;
        provider = left;
        group = right.Substring(0, dot);
        alarmName = right.Substring(dot + 1);
        return true;
    }

    private static string DescribeTransition(OnAlarmTransitionEvent body)
    {
        return string.Format(
            "kind={0} ref='{1}' source='{2}' type='{3}' severity={4} operator='{5}' comment='{6}' ts={7:o}",
            body.TransitionKind, body.AlarmFullReference, body.SourceObjectReference,
            body.AlarmTypeName, body.Severity, body.OperatorUser, body.OperatorComment,
            body.TransitionTimestamp?.ToDateTime() ?? DateTime.MinValue);
    }

    private static string DescribeSnapshot(ActiveAlarmSnapshot s)
    {
        return string.Format(
            "ref='{0}' state={1} severity={2} operator='{3}' comment='{4}' ts={5:o}",
            s.AlarmFullReference, s.CurrentState, s.Severity, s.OperatorUser,
            s.OperatorComment,
            s.LastTransitionTimestamp?.ToDateTime() ?? DateTime.MinValue);
    }

    private void Log(string line)
    {
        log.Enqueue($"[t={elapsed.Elapsed.TotalSeconds:F3}s] {line}");
    }
}
