// AlarmSubtagLiveSmokeTests.cs
//
// Validates the subtag-fallback pipeline against a real Galaxy + MXAccess install:
//   LmxSubtagAlarmSource (own LMXProxyServerClass) ->
//   SubtagAlarmConsumer (state machine + AcknowledgeByName write) ->
//   synthesized MxAlarmTransitionEvent (Raise / Clear, Degraded=true, SyntheticGuid)
//
// FIELD NAMES CONFIRMED: InAlarm/Acked/AckMsg/Priority are the confirmed AVEVA
// AlarmExtension primitive field names, verified by querying the live ZB Galaxy
// attribute_definition rows. The remaining open item for live validation is
// confirming the runtime item reference path — i.e. that
// "<Object>.<AlarmAttr>.InAlarm" is the correct MXAccess path with no
// intermediate alarm-condition segment.
//
// HOW TO RUN:
//   1. On the dev rig with AVEVA System Platform installed and Galaxy running:
//      $env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = "1"
//   2. Remove (or set to null) the Skip parameter on the [Fact] below.
//   3. Drive a TestMachine alarm so its Active/Acked subtags toggle — either an
//      alarm flip script (same one used by AlarmsLiveSmokeTests, ~10 s cadence)
//      or a manual operator/IDE toggle of the alarm attribute. The rig's
//      TestAlarm attributes are object-driven, so an external MXAccess Write
//      cannot toggle them (confirmed live 2026-06-14 by toggling TestAlarm002
//      from the IDE).
//
// net48/x86 constraints:
//   - No init-only properties, records, index/range operators, C# 8+ pattern
//     matches beyond what the existing Worker.Tests files use.
//   - All Booleans from MXAccess arrive as boxed int (0 / non-zero); coerce
//     with Convert.ToBoolean or cast to int first.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using ZB.MOM.WW.MxGateway.Contracts.Proto;
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
using ZB.MOM.WW.MxGateway.Worker.Tests.TestSupport;
using Xunit.Abstractions;

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

/// <summary>
///     Live dev-rig smoke test for the subtag-fallback alarm pipeline.
///     Confirms that <see cref="LmxSubtagAlarmSource"/> wired to a real
///     <c>LMXProxyServerClass</c> + <see cref="SubtagAlarmConsumer"/> can
///     synthesize <see cref="MxAlarmTransitionEvent"/> records from Galaxy
///     alarm subtags using the confirmed AVEVA <c>AlarmExtension</c> field
///     names (InAlarm/Acked/AckMsg/Priority), and that
///     <see cref="SubtagAlarmConsumer.AcknowledgeByName"/> writes the
///     ack-comment subtag (AckMsg) successfully.
///
///     Skip-gated; flip <c>Skip=null</c> on the dev rig with an alarm being
///     driven (flip script or a manual operator/IDE toggle of the alarm
///     attribute). The remaining live-validation item is confirming that
///     the runtime MXAccess item reference path requires no intermediate
///     alarm-condition segment (i.e. <c>&lt;Object&gt;.&lt;AlarmAttr&gt;.InAlarm</c>
///     resolves as-is).
/// </summary>
public sealed class AlarmSubtagLiveSmokeTests
{
    // -------------------------------------------------------------------------
    // Subtag addresses for TestMachine_001.TestAlarm001.
    //
    // Field names (InAlarm/Acked/AckMsg/Priority) are CONFIRMED against the live
    // ZB Galaxy AlarmExtension primitive attribute_definition rows. The subtag
    // address format is <ObjectTagName>.<AlarmAttributeName>. The remaining
    // live-validation item is confirming that the MXAccess runtime item
    // reference resolves without an intermediate alarm-condition segment
    // (i.e. "<Object>.<AlarmAttr>.InAlarm" resolves as-is).
    // -------------------------------------------------------------------------

    /// <summary>The Galaxy provider expression used by the existing live smoke tests.</summary>
    private static readonly string Provider =
        string.Format(@"\\{0}\Galaxy", Environment.MachineName);

    /// <summary>The Galaxy group (provider sub-path) containing the test alarm.</summary>
    private const string Group = "DEV";

    /// <summary>The test alarm's tag name as the dispatcher composes it (Group.TagName).</summary>
    private const string AlarmTagName = "TestMachine_001.TestAlarm001";

    // Confirmed AVEVA AlarmExtension field name (in-alarm boolean).
    private const string PlaceholderActiveSubtag = "TestMachine_001.TestAlarm001.InAlarm";

    // Confirmed AVEVA AlarmExtension field name (acknowledged boolean).
    private const string PlaceholderAckedSubtag = "TestMachine_001.TestAlarm001.Acked";

    // Confirmed AVEVA AlarmExtension field name (ack-comment write target).
    // Writing this subtag performs the acknowledge in AVEVA.
    private const string PlaceholderAckCommentSubtag = "TestMachine_001.TestAlarm001.AckMsg";

    // Confirmed AVEVA AlarmExtension field name (alarm priority/severity).
    private const string PlaceholderPrioritySubtag = "TestMachine_001.TestAlarm001.Priority";

    // -------------------------------------------------------------------------

    private static readonly TimeSpan RaiseWaitTimeout = TimeSpan.FromSeconds(30);
    private static readonly TimeSpan ClearWaitTimeout = TimeSpan.FromSeconds(30);
    private static readonly string AlarmFullReference =
        AlarmRecordTransitionMapper.ComposeFullReference(Provider, Group, AlarmTagName);

    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 AlarmSubtagLiveSmokeTests class.</summary>
    /// <param name="output">Test output helper for logging.</param>
    public AlarmSubtagLiveSmokeTests(ITestOutputHelper output)
    {
        this.output = output;
    }

    /// <summary>
    ///     Verifies the subtag-fallback pipeline: advises alarm subtags through a
    ///     real <c>LMXProxyServerClass</c>, collects a Raise then a Clear
    ///     transition synthesized by <see cref="SubtagAlarmConsumer"/>, confirms
    ///     the Degraded flag and synthetic GUID are stamped, then
    ///     AcknowledgeByName and verifies the ack-comment write returns 0.
    /// </summary>
    [Fact(Skip = "Live dev-rig smoke test — flip Skip=null with AVEVA + an alarm being driven (flip script or manual operator/IDE toggle of the alarm attribute). Subtag fallback path. Field names confirmed (InAlarm/Acked/AckMsg/Priority); live-validate runtime path resolves without intermediate alarm-condition segment.")]
    public void SubtagFallback_FullPipelineRoundTrip_SynthesizesRaiseAndAcknowledges()
    {
        Exception? threadException = null;
        ManualResetEventSlim done = new ManualResetEventSlim(false);
        Thread 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(string.Format("Captured {0} log line(s):", log.Count));
        string? line;
        while (log.TryDequeue(out line))
        {
            output.WriteLine(line);
        }

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

    /// <summary>
    ///     Runtime-path resolution probe. Unlike the full lifecycle test this
    ///     does NOT require an active alarm: it advises the four subtags and
    ///     observes the initial values MXAccess delivers on advise, then writes
    ///     the AckMsg subtag. It confirms the runtime item-reference path
    ///     (<c>&lt;Object&gt;.&lt;AlarmAttr&gt;.&lt;field&gt;</c>) resolves with
    ///     no intermediate alarm-condition segment — the last open item from the
    ///     design. Runs only when <c>MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1</c>.
    ///     Reports "inconclusive" (without failing) when no values are delivered,
    ///     e.g. the Galaxy engine for the test object is not running.
    /// </summary>
    [LiveMxAccessFact]
    public void LiveProbe_AlarmSubtagsResolve_AndAckMsgWriteSucceeds()
    {
        Exception? threadException = null;
        ManualResetEventSlim done = new ManualResetEventSlim(false);
        Thread 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();

        List<string> lines = new List<string>();
        output.WriteLine(string.Format("Captured {0} log line(s):", log.Count));
        string? line;
        while (log.TryDequeue(out line))
        {
            output.WriteLine(line);
            lines.Add(line);
        }

        // Also persist to a file so the evidence survives shell-logger quirks
        // when run over SSH. Path overridable via PROBE_LOG.
        string logPath = Environment.GetEnvironmentVariable("PROBE_LOG")
            ?? System.IO.Path.Combine(System.IO.Path.GetTempPath(), "alarm-subtag-probe.log");
        try { System.IO.File.WriteAllLines(logPath, lines); }
        catch (Exception writeEx) { output.WriteLine("probe-log write failed: " + writeEx.Message); }

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

    private void RunProbe()
    {
        Log("=== Subtag runtime-path resolution probe ===");
        Log("AlarmFullReference: " + AlarmFullReference);
        Log("Subtag addresses under test:");
        Log("  active  = " + PlaceholderActiveSubtag);
        Log("  acked   = " + PlaceholderAckedSubtag);
        Log("  ackMsg  = " + PlaceholderAckCommentSubtag);
        Log("  priority= " + PlaceholderPrioritySubtag);

        AlarmSubtagTarget target = new AlarmSubtagTarget
        {
            AlarmFullReference = AlarmFullReference,
            SourceObjectReference = AlarmTagName,
            ActiveSubtag = PlaceholderActiveSubtag,
            AckedSubtag = PlaceholderAckedSubtag,
            AckCommentSubtag = PlaceholderAckCommentSubtag,
            PrioritySubtag = PlaceholderPrioritySubtag,
        };
        List<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget> { target };

        MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
        LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);
        using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList);

        // Hook the RAW source so every advised subtag's value-change is seen,
        // including the initial value MXAccess delivers on advise (the state
        // machine itself only emits on active/acked transitions).
        Dictionary<string, string> lastValues =
            new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
        source.ValueChanged += (_, c) =>
        {
            string v = c.Value == null
                ? "<null>"
                : string.Format("{0} ({1})", c.Value, c.Value.GetType().Name);
            lastValues[c.ItemAddress] = v;
            Log(string.Format("ValueChanged: {0} = {1}", c.ItemAddress, v));
        };

        Log("Subscribe (advise InAlarm/Acked/Priority) ...");
        consumer.Subscribe(string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group));
        Log("Subscribe returned OK.");

        // Pump the STA for ~6s to receive each advised subtag's initial value.
        DateTime deadline = DateTime.UtcNow + TimeSpan.FromSeconds(6);
        while (DateTime.UtcNow < deadline)
        {
            PumpMessages();
            Thread.Sleep(100);
        }

        Log(string.Format("Distinct subtags that delivered a value: {0}", lastValues.Count));
        bool inAlarmResolved = lastValues.ContainsKey(PlaceholderActiveSubtag);
        Log("InAlarm subtag delivered a value: " + inAlarmResolved);

        // Decisive write: AckMsg. Returns 0 only if the address resolves and is writable.
        Log("AcknowledgeByName (writes AckMsg subtag) ...");
        int rc = consumer.AcknowledgeByName(
            alarmName: AlarmTagName,
            providerName: Provider,
            groupName: Group,
            ackComment: "subtag-resolution-probe",
            ackOperatorName: Environment.UserName,
            ackOperatorNode: Environment.MachineName,
            ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
            ackOperatorFullName: Environment.UserName);
        Log(string.Format("AcknowledgeByName(AckMsg write) -> rc={0}", rc));

        DateTime settle = DateTime.UtcNow + TimeSpan.FromSeconds(2);
        while (DateTime.UtcNow < settle)
        {
            PumpMessages();
            Thread.Sleep(100);
        }

        if (lastValues.Count == 0)
        {
            Log("INCONCLUSIVE: no subtag values delivered within the window. The Galaxy "
                + "engine hosting TestMachine_001 may be stopped, or the runtime path needs "
                + "an intermediate segment. Re-run with the engine running. (Field names "
                + "remain confirmed from the ZB AlarmExtension model.)");
            return;
        }

        // Live data arrived — assert the in-alarm subtag resolved and the ack write succeeded.
        Assert.True(inAlarmResolved,
            "InAlarm subtag delivered no value while other subtags did — the runtime path for InAlarm may differ.");
        Assert.Equal(0, rc);
        Log("CONFIRMED: runtime subtag path resolves and the AckMsg write succeeded.");
    }

    private void RunSmoke()
    {
        Log(string.Format("AlarmFullReference: {0}", AlarmFullReference));
        Log("VERIFY: PlaceholderActiveSubtag = " + PlaceholderActiveSubtag);
        Log("VERIFY: PlaceholderAckedSubtag  = " + PlaceholderAckedSubtag);
        Log("VERIFY: PlaceholderAckCommentSubtag = " + PlaceholderAckCommentSubtag);
        Log("VERIFY: PlaceholderPrioritySubtag   = " + PlaceholderPrioritySubtag);
        Log(string.Format("RaiseWaitTimeout={0}s  ClearWaitTimeout={1}s",
            RaiseWaitTimeout.TotalSeconds, ClearWaitTimeout.TotalSeconds));

        // Build target with confirmed AVEVA AlarmExtension subtag names.
        // InAlarm/Acked/AckMsg/Priority confirmed against live ZB Galaxy
        // attribute_definition rows. Remaining live-validation: runtime path resolves
        // without an intermediate alarm-condition segment.
        AlarmSubtagTarget target = new AlarmSubtagTarget
        {
            AlarmFullReference = AlarmFullReference,
            SourceObjectReference = AlarmTagName,
            ActiveSubtag = PlaceholderActiveSubtag,
            AckedSubtag = PlaceholderAckedSubtag,
            AckCommentSubtag = PlaceholderAckCommentSubtag,
            PrioritySubtag = PlaceholderPrioritySubtag,
        };

        List<AlarmSubtagTarget> watchList = new List<AlarmSubtagTarget> { target };

        // Construct the real COM-backed subtag source using the production
        // factory (MxAccessComObjectFactory -> new LMXProxyServerClass()).
        // This is the same factory the worker uses in production; no test
        // double is involved on this path.
        MxAccessComObjectFactory factory = new MxAccessComObjectFactory();
        LmxSubtagAlarmSource source = new LmxSubtagAlarmSource(factory, clientName: null);

        // SubtagAlarmConsumer wraps the source and drives the state machine.
        using SubtagAlarmConsumer consumer = new SubtagAlarmConsumer(source, watchList);

        // Collect emitted transitions on the STA (handler fires on the same
        // STA that services the COM OnDataChange callback).
        ConcurrentQueue<MxAlarmTransitionEvent> transitions =
            new ConcurrentQueue<MxAlarmTransitionEvent>();
        consumer.AlarmTransitionEmitted += (_, e) =>
        {
            Log(string.Format("Transition emitted: {0}", DescribeTransition(e)));
            transitions.Enqueue(e);
        };

        // Subscribe binds Advise on all observable subtags (Active, Acked,
        // Priority). The subscription expression is unused in subtag mode; pass
        // something recognizable for diagnostics.
        string subscriptionExpression = string.Format(@"\\{0}\Galaxy!{1}", Environment.MachineName, Group);
        Log(string.Format("Calling Subscribe({0}) ...", subscriptionExpression));
        consumer.Subscribe(subscriptionExpression);
        Log("Subscribe returned OK.");

        // 1. Wait for a Raise transition.  Whatever is driving the alarm — a
        //    flip script (same one used by AlarmsLiveSmokeTests, ~10 s cadence)
        //    or a manual operator/IDE toggle — writes the active subtag.
        //    LmxSubtagAlarmSource delivers OnDataChange via
        //    the Windows message pump on the STA, so we must pump messages
        //    here while we wait — mirroring how AlarmsLiveSmokeTests drives
        //    its WnWrapAlarmConsumer.PollOnce() from the STA in a tight loop.
        Log(string.Format("Waiting up to {0}s for a Raise transition ...", RaiseWaitTimeout.TotalSeconds));
        MxAlarmTransitionEvent? raiseEvent = WaitForTransitionKind(
            transitions, AlarmTransitionKind.Raise, RaiseWaitTimeout, "Raise");

        Assert.NotNull(raiseEvent);
        Assert.True(raiseEvent!.Record.Degraded,
            "Subtag-synthesized records must have Degraded=true.");
        // SubtagAlarmConsumer must stamp a synthetic GUID on the transition.
        Assert.NotEqual(Guid.Empty, raiseEvent.Record.AlarmGuid);
        // A Raise transition must leave the record in UnackAlm state.
        Assert.Equal(MxAlarmStateKind.UnackAlm, raiseEvent.Record.State);
        Log(string.Format("Raise confirmed: AlarmGuid={0} Degraded={1} State={2}",
            raiseEvent.Record.AlarmGuid, raiseEvent.Record.Degraded, raiseEvent.Record.State));

        // 2. Snapshot active alarms and confirm the raised alarm is present.
        IReadOnlyList<MxAlarmSnapshotRecord> snapshot = consumer.SnapshotActiveAlarms(out _);
        Log(string.Format("SnapshotActiveAlarms count={0}", snapshot.Count));
        foreach (MxAlarmSnapshotRecord s in snapshot)
        {
            Log(string.Format("  snapshot: TagName='{0}' Group='{1}' State={2} Degraded={3} Guid={4}",
                s.TagName, s.Group, s.State, s.Degraded, s.AlarmGuid));
        }

        bool foundInSnapshot = false;
        foreach (MxAlarmSnapshotRecord s in snapshot)
        {
            if (string.Equals(
                AlarmRecordTransitionMapper.ComposeFullReference(s.ProviderName, s.Group, s.TagName),
                AlarmFullReference,
                StringComparison.OrdinalIgnoreCase))
            {
                foundInSnapshot = true;
                break;
            }
        }

        Assert.True(foundInSnapshot,
            string.Format("Active alarm snapshot must contain '{0}' after a Raise.", AlarmFullReference));

        // 3. AcknowledgeByName — writes the ack-comment subtag.
        //    The dispatcher derives (alarmName, provider, group) via the same
        //    TryParseReference logic as AlarmsLiveSmokeTests.
        Log("Calling AcknowledgeByName ...");
        int rc = consumer.AcknowledgeByName(
            alarmName: AlarmTagName,
            providerName: Provider,
            groupName: Group,
            ackComment: "subtag-fallback-smoke ack",
            ackOperatorName: Environment.UserName,
            ackOperatorNode: Environment.MachineName,
            ackOperatorDomain: Environment.UserDomainName ?? string.Empty,
            ackOperatorFullName: Environment.UserName);

        Log(string.Format("AcknowledgeByName -> rc={0}", rc));
        Assert.Equal(0, rc);

        // 4. Wait for a Clear or Acknowledge transition to confirm the state
        //    machine continues tracking after the ack write.
        Log(string.Format("Waiting up to {0}s for a Clear or Acknowledge transition ...",
            ClearWaitTimeout.TotalSeconds));
        MxAlarmTransitionEvent? postAckEvent = WaitForAnyTransition(transitions, ClearWaitTimeout, "post-ack");

        // A null here is not a hard failure: the test alarm may not have
        // cleared within the window, and the ack-comment write already
        // confirmed the subtag path is wired.  Log and assert non-null
        // only when the post-ack transition is expected.
        if (postAckEvent != null)
        {
            AlarmTransitionKind postAckKind = AlarmRecordTransitionMapper.MapTransition(
                postAckEvent.PreviousState, postAckEvent.Record.State);
            Log(string.Format("Post-ack transition: {0}", DescribeTransition(postAckEvent)));
            Assert.NotEqual(AlarmTransitionKind.Unspecified, postAckKind);
        }
        else
        {
            Log("No post-ack transition within timeout — ack-comment write succeeded; state machine still live.");
        }

        Log("Smoke test complete.");
    }

    private MxAlarmTransitionEvent? WaitForTransitionKind(
        ConcurrentQueue<MxAlarmTransitionEvent> queue,
        AlarmTransitionKind kind,
        TimeSpan timeout,
        string label)
    {
        DateTime deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            // Pump the STA Windows message queue so COM OnDataChange callbacks
            // can be delivered.  LMXProxyServerClass is apartment-threaded and
            // requires the STA to be pumping; a bare Thread.Sleep would stall
            // the pump.
            PumpMessages();

            MxAlarmTransitionEvent? evt;
            if (queue.TryDequeue(out evt) && evt != null)
            {
                AlarmTransitionKind evtKind = AlarmRecordTransitionMapper.MapTransition(
                    evt.PreviousState, evt.Record.State);
                if (evtKind == kind)
                {
                    return evt;
                }

                Log(string.Format("Skipped transition kind={0} while waiting for {1}.", evtKind, label));
                // Can't re-enqueue; log and continue.
            }

            Thread.Sleep(250);
        }

        Log(string.Format("Timed out waiting for {0} transition after {1}s.", label, timeout.TotalSeconds));
        return null;
    }

    private MxAlarmTransitionEvent? WaitForAnyTransition(
        ConcurrentQueue<MxAlarmTransitionEvent> queue,
        TimeSpan timeout,
        string label)
    {
        DateTime deadline = DateTime.UtcNow + timeout;
        while (DateTime.UtcNow < deadline)
        {
            PumpMessages();

            MxAlarmTransitionEvent? evt;
            if (queue.TryDequeue(out evt) && evt != null)
            {
                return evt;
            }

            Thread.Sleep(250);
        }

        Log(string.Format("Timed out waiting for any transition ({0}) after {1}s.", label, timeout.TotalSeconds));
        return null;
    }

    /// <summary>
    ///     Runs a single pass of the Windows STA message pump using a
    ///     non-blocking PeekMessage/DispatchMessage loop so COM
    ///     <c>OnDataChange</c> callbacks from <c>LMXProxyServerClass</c>
    ///     (ThreadingModel=Apartment) can be delivered on this thread.
    ///     Mirrors the pump pattern documented in
    ///     <c>docs/MxAccessWorkerInstanceDesign.md</c>.
    /// </summary>
    private static void PumpMessages()
    {
        NativeMethods.MSG msg;
        // Drain all currently posted messages; return as soon as the queue
        // is empty.
        while (NativeMethods.PeekMessage(out msg, IntPtr.Zero, 0, 0, NativeMethods.PM_REMOVE))
        {
            NativeMethods.TranslateMessage(ref msg);
            NativeMethods.DispatchMessage(ref msg);
        }
    }

    private static string DescribeTransition(MxAlarmTransitionEvent e)
    {
        AlarmTransitionKind kind = AlarmRecordTransitionMapper.MapTransition(
            e.PreviousState, e.Record.State);
        return string.Format(
            "Kind={0} PreviousState={1} State={2} TagName='{3}' Group='{4}' Provider='{5}' Degraded={6} Guid={7}",
            kind,
            e.PreviousState,
            e.Record.State,
            e.Record.TagName,
            e.Record.Group,
            e.Record.ProviderName,
            e.Record.Degraded,
            e.Record.AlarmGuid);
    }

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

    // -------------------------------------------------------------------------
    // Minimal P/Invoke shim so the STA pump can be driven without pulling in
    // the full StaRuntime machinery from the Worker project.  The signatures
    // mirror those in MxAccessStaRuntime and are well-known Win32.
    // -------------------------------------------------------------------------
    private static class NativeMethods
    {
        internal const uint PM_REMOVE = 0x0001;

        [StructLayout(LayoutKind.Sequential)]
        internal struct MSG
        {
            internal IntPtr hwnd;
            internal uint message;
            internal IntPtr wParam;
            internal IntPtr lParam;
            internal uint time;
            internal int ptX;
            internal int ptY;
        }

        /// <summary>Checks the thread message queue for a message, optionally removing it.</summary>
        /// <param name="lpMsg">Receives the retrieved message.</param>
        /// <param name="hWnd">Window handle to filter messages for, or <see cref="IntPtr.Zero"/> for any window owned by the calling thread.</param>
        /// <param name="wMsgFilterMin">Lowest message value to examine.</param>
        /// <param name="wMsgFilterMax">Highest message value to examine.</param>
        /// <param name="wRemoveMsg">Specifies how the message is handled (e.g. <see cref="PM_REMOVE"/>).</param>
        /// <returns><see langword="true"/> if a message is available; otherwise <see langword="false"/>.</returns>
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool PeekMessage(
            out MSG lpMsg,
            IntPtr hWnd,
            uint wMsgFilterMin,
            uint wMsgFilterMax,
            uint wRemoveMsg);

        /// <summary>Translates virtual-key messages into character messages.</summary>
        /// <param name="lpMsg">The message to translate.</param>
        /// <returns><see langword="true"/> if the message was translated; otherwise <see langword="false"/>.</returns>
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool TranslateMessage(ref MSG lpMsg);

        /// <summary>Dispatches a message to the window procedure.</summary>
        /// <param name="lpmsg">The message to dispatch.</param>
        /// <returns>The value returned by the window procedure.</returns>
        [DllImport("user32.dll")]
        internal static extern IntPtr DispatchMessage(ref MSG lpmsg);
    }
}
