using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using WNWRAPCONSUMERLib;
using Xunit.Abstractions;

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

/// <summary>
///     Runtime probe — instantiate AVEVA's standalone wnwrapConsumer COM
///     class (CLSID 7AB52E5F-36B2-4A30-AE46-952A746F667C, registered at
///     C:\Program Files (x86)\Common Files\ArchestrA\wnwrapConsumer.dll),
///     subscribe to the dev rig's `\\&lt;machine&gt;\Galaxy!DEV` provider, and
///     poll <c>GetXmlCurrentAlarms2</c> while a System Platform script flips
///     <c>TestMachine_001.TestAlarm001</c> every 10s. The XML payload bypasses
///     the FILETIME→DateTime auto-marshaling that crashes
///     <c>aaAlarmManagedClient.AlarmClient.GetHighPriAlarm</c>.
///
///     Skip-gated; flip Skip=null to run on the dev rig.
/// </summary>
public sealed class WnWrapConsumerProbeTests
{
    private static readonly string MachineName = Environment.MachineName;
    private static readonly string SubscriptionExpression =
        $@"\\{MachineName}\Galaxy!DEV";

    // XML query form — per ArchestrA reference. NODE is the
    // machine, PROVIDER is the literal "Galaxy", GROUP is the area.
    private static readonly string XmlAlarmQuery =
        "<QUERIES FROM_PRIORITY=\"1\" TO_PRIORITY=\"999\" ALARM_STATE=\"ALL\" DISPLAY_MODE=\"Summary\">" +
        "<QUERY>" +
        $"<NODE>{Environment.MachineName}</NODE>" +
        "<PROVIDER>Galaxy</PROVIDER>" +
        "<GROUP>DEV</GROUP>" +
        "</QUERY>" +
        "</QUERIES>";

    private const int MaxAlarmsPerFetch = 100;
    private static readonly TimeSpan PumpDuration = TimeSpan.FromSeconds(30);
    private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(500);

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

    /// <summary>Initializes a new probe test with the given test output helper.</summary>
    /// <param name="output">The xUnit test output helper.</param>
    public WnWrapConsumerProbeTests(ITestOutputHelper output)
    {
        this.output = output;
    }

    /// <summary>Probes wnwrap consumer on dev rig and logs XML alarm stream output.</summary>
    [Fact(Skip = "Runtime probe — flip Skip=null on the dev rig (AVEVA installed) to capture wnwrapConsumer XML alarm output. Verified working 2026-05-01.")]
    public void ProbeWnWrapConsumer_OnDevRig_LogsXmlAlarmStream()
    {
        Exception? threadException = null;
        var done = new ManualResetEventSlim(false);
        var thread = new Thread(() =>
        {
            try { RunProbe(); }
            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 RunProbe()
    {
        wwAlarmConsumerClass? client = null;
        try
        {
            Log("Creating wwAlarmConsumerClass via CoCreateInstance...");
            client = new wwAlarmConsumerClass();
            Log($"Instantiated. RuntimeType={client.GetType().FullName}");

            // Lifecycle: per AlarmClientDiscovery.md finding, InitializeConsumer
            // MUST precede RegisterConsumer for the alarm provider to become
            // visible. The wnwrap surface mirrors that requirement.
            try
            {
                int init = client.InitializeConsumer("MxGatewayProbe.WnWrap");
                Log($"InitializeConsumer -> {init}");
            }
            catch (Exception ex)
            {
                Log($"InitializeConsumer threw: {ex.GetType().Name}: {ex.Message}");
            }

            try
            {
                // hWnd=0 — XML pull-based; no message pump needed.
                int reg = client.RegisterConsumer(
                    hWnd: 0,
                    szProductName: "MxGatewayProbe",
                    szApplicationName: "MxGatewayProbe.WnWrap",
                    szVersion: "1.0");
                Log($"RegisterConsumer(hWnd=0) -> {reg}");
            }
            catch (Exception ex)
            {
                Log($"RegisterConsumer threw: {ex.GetType().Name}: {ex.Message}");
            }

            // Try both subscription mechanisms: classic Subscribe (canonical
            // scope from prior aaAlarmManagedClient probe), and
            // SetXmlAlarmQuery (the wnwrap-native filter format).
            try
            {
                int sub = client.Subscribe(
                    szSubscription: SubscriptionExpression,
                    wFromPri: 1,
                    wToPri: 999,
                    QueryType: eQueryType.qtSummary,
                    SortFlags: eSortFlags.sfReturnNewestFirst,
                    FilterMask: eAlarmFilterState.asAlarmActiveNow,
                    FilterSpecification: eAlarmFilterState.asAlarmActiveNow);
                Log($"Subscribe('{SubscriptionExpression}') -> {sub}");
            }
            catch (Exception ex)
            {
                Log($"Subscribe threw: {ex.GetType().Name}: {ex.Message}");
            }

            try
            {
                Log($"SetXmlAlarmQuery payload: {XmlAlarmQuery}");
                client.SetXmlAlarmQuery(XmlAlarmQuery);
                Log("SetXmlAlarmQuery -> ok");
            }
            catch (Exception ex)
            {
                Log($"SetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}");
            }

            // Echo the query back so we can confirm what the consumer is
            // actually filtering on (provider may rewrite or reject some
            // attributes silently).
            try
            {
                object echo = string.Empty;
                client.GetXmlAlarmQuery(out echo);
                Log($"GetXmlAlarmQuery (round-trip) -> {Truncate(echo?.ToString() ?? "<null>", 600)}");
            }
            catch (Exception ex)
            {
                Log($"GetXmlAlarmQuery threw: {ex.GetType().Name}: {ex.Message}");
            }

            // Pump phase: poll GetXmlCurrentAlarms2 every PollInterval; log on
            // every change in payload. Run for PumpDuration. The user's flip
            // script writes TestMachine_001.TestAlarm001 every 10s; expect at
            // least 2-3 transitions over a 30s window.
            Log($"Polling GetXmlCurrentAlarms2 every {PollInterval.TotalMilliseconds:F0}ms for {PumpDuration.TotalSeconds:F0}s.");
            DateTime deadline = DateTime.UtcNow + PumpDuration;
            DateTime nextPoll = DateTime.UtcNow;
            int pollCount = 0;
            string lastV2 = string.Empty;
            string lastV1 = string.Empty;
            int v2Ok = 0, v2Throw = 0, v1Ok = 0, v1Throw = 0;
            int statsOk = 0, statsThrow = 0;
            string lastStats = string.Empty;
            while (DateTime.UtcNow < deadline)
            {
                if (DateTime.UtcNow >= nextPoll)
                {
                    pollCount++;

                    // V2 channel.
                    try
                    {
                        object xml2 = string.Empty;
                        client.GetXmlCurrentAlarms2(MaxAlarmsPerFetch, out xml2);
                        v2Ok++;
                        string s = xml2?.ToString() ?? "<null>";
                        if (s != lastV2)
                        {
                            Log($"GetXmlCurrentAlarms2 #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}");
                            lastV2 = s;
                        }
                    }
                    catch (Exception ex)
                    {
                        v2Throw++;
                        string es = $"{ex.GetType().Name}: {ex.Message}";
                        if (es != lastV2)
                        {
                            Log($"GetXmlCurrentAlarms2 #{pollCount} threw: {es}");
                            lastV2 = es;
                        }
                    }

                    // V1 channel — different vtable slot; either may be the
                    // populated one in this AVEVA build.
                    try
                    {
                        object xml1 = string.Empty;
                        client.GetXmlCurrentAlarms(MaxAlarmsPerFetch, out xml1);
                        v1Ok++;
                        string s = xml1?.ToString() ?? "<null>";
                        if (s != lastV1)
                        {
                            Log($"GetXmlCurrentAlarms #{pollCount} (CHANGED, len={s.Length}): {Truncate(s, 1200)}");
                            lastV1 = s;
                        }
                    }
                    catch (Exception ex)
                    {
                        v1Throw++;
                        string es = $"{ex.GetType().Name}: {ex.Message}";
                        if (es != lastV1)
                        {
                            Log($"GetXmlCurrentAlarms #{pollCount} threw: {es}");
                            lastV1 = es;
                        }
                    }

                    // Stats channel — heartbeat + active-count even if the XML
                    // calls are dry, this surfaces whether wnwrap sees any
                    // alarms in the subscribed scope at all.
                    try
                    {
                        int pct, total, active, newAlms, changes;
                        client.GetStatistics(
                            out pct, out total, out active, out newAlms, out changes,
                            IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
                        statsOk++;
                        string statsSummary = $"pct={pct} total={total} active={active} new={newAlms} changes={changes}";
                        if (statsSummary != lastStats)
                        {
                            Log($"GetStatistics #{pollCount} (CHANGED): {statsSummary}");
                            lastStats = statsSummary;
                        }
                    }
                    catch (Exception ex)
                    {
                        statsThrow++;
                        Log($"GetStatistics #{pollCount} threw: {ex.GetType().Name}: {ex.Message}");
                    }

                    nextPoll = DateTime.UtcNow + PollInterval;
                }
                Thread.Sleep(20);
            }
            Log($"Pump done. Tally: v2 ok={v2Ok} threw={v2Throw}, v1 ok={v1Ok} threw={v1Throw}, stats ok={statsOk} threw={statsThrow}");

            try { int dereg = client.DeregisterConsumer(); Log($"DeregisterConsumer -> {dereg}"); }
            catch (Exception ex) { Log($"DeregisterConsumer threw: {ex.GetType().Name}: {ex.Message}"); }

            try { int uninit = client.UninitializeConsumer(); Log($"UninitializeConsumer -> {uninit}"); }
            catch (Exception ex) { Log($"UninitializeConsumer threw: {ex.GetType().Name}: {ex.Message}"); }
        }
        finally
        {
            if (client != null && Marshal.IsComObject(client))
            {
                try { Marshal.FinalReleaseComObject(client); } catch { /* swallow */ }
            }
        }
    }

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

    private static string Truncate(string s, int max)
    {
        if (string.IsNullOrEmpty(s) || s.Length <= max) return s ?? string.Empty;
        return s.Substring(0, max) + $"…[+{s.Length - max} chars]";
    }
}
