using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;

namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Emulate;

/// <summary>
///     Golden-box-tier ALMD alarm projection tests against Logix Emulate.
///     Promotes the feature-flagged ALMD projection (task #177) from unit-only coverage
///     (<c>AbCipAlarmProjectionTests</c> with faked InFaulted sequences) to end-to-end
///     wire-level coverage — Emulate runs the real ALMD instruction, with real
///     rising-edge semantics on <c>InFaulted</c> + <c>Ack</c>, so the driver's poll-based
///     projection gets validated against the actual behaviour shops running FT View see.
/// </summary>
/// <remarks>
///     <para><b>Required Emulate project state</b> (see <c>LogixProject/README.md</c>):</para>
///     <list type="bullet">
///       <item>Controller-scope ALMD tag <c>HighTempAlarm</c> — a standard ALMD instruction
///         with default member set (<c>In</c>, <c>InFaulted</c>, <c>Acked</c>,
///         <c>Severity</c>, <c>Cfg_ProgTime</c>, …).</item>
///       <item>A periodic task that drives <c>HighTempAlarm.In</c> false→true→false at a
///         cadence the operator can script via a one-shot routine (e.g. a
///         <c>SimulateAlarm</c> bit the test case pulses through
///         <c>IWritable.WriteAsync</c>).</item>
///       <item>Operator writes <c>1</c> to <c>SimulateAlarm</c> to trigger the rising
///         edge on <c>HighTempAlarm.In</c>; ladder uses that as the alarm input.</item>
///     </list>
///     <para>Runs only when <c>AB_SERVER_PROFILE=emulate</c>. ab_server has no ALMD
///     instruction + no alarm subsystem, so this tier-gated class couldn't produce a
///     meaningful result against the default simulator.</para>
///     <para>The Emulate tier is hardware-gated (Rockwell per-seat license, Windows-only,
///     conflicts with Docker Desktop's WSL 2 backend) so a permanent skip in CI is expected
///     — see <c>docs/drivers/AbServer-Test-Fixture.md</c> §"Logix Emulate golden-box tier"
///     for the full rationale + the gap matrix this test closes.</para>
/// </remarks>
[Collection("AbServerEmulate")]
[Trait("Category", "Integration")]
[Trait("Tier", "Emulate")]
public sealed class AbCipEmulateAlmdTests
{
    /// <summary>Verifies that real ALMD raise fires OnAlarmEvent through the driver projection.</summary>
    [AbServerFact]
    public async Task Real_ALMD_raise_fires_OnAlarmEvent_through_the_driver_projection()
    {
        AbServerProfileGate.SkipUnless(AbServerProfileGate.Emulate);

        var endpoint = Environment.GetEnvironmentVariable("AB_SERVER_ENDPOINT")
            ?? throw new InvalidOperationException(
                "AB_SERVER_ENDPOINT must be set to the Logix Emulate instance when AB_SERVER_PROFILE=emulate.");

        var options = new AbCipDriverOptions
        {
            Devices = [new AbCipDeviceOptions($"ab://{endpoint}/1,0")],
            EnableAlarmProjection = true,
            AlarmPollInterval = TimeSpan.FromMilliseconds(200),
            RawTags = AbCipRawTags.From(
                new AbCipTagDefinition(
                    Name: "HighTempAlarm",
                    DeviceHostAddress: $"ab://{endpoint}/1,0",
                    TagPath: "HighTempAlarm",
                    DataType: AbCipDataType.Structure,
                    Members: [
                        new AbCipStructureMember("InFaulted", AbCipDataType.DInt),
                        new AbCipStructureMember("Acked",     AbCipDataType.DInt),
                        new AbCipStructureMember("Severity",  AbCipDataType.DInt),
                        new AbCipStructureMember("In",        AbCipDataType.DInt),
                    ]),
                // The "simulate the alarm input" bit the ladder watches.
                new AbCipTagDefinition(
                    Name: "SimulateAlarm",
                    DeviceHostAddress: $"ab://{endpoint}/1,0",
                    TagPath: "SimulateAlarm",
                    DataType: AbCipDataType.Bool,
                    Writable: true)),
        };

        await using var drv = new AbCipDriver(options, driverInstanceId: "emulate-almd");
        await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);

        var raised = new TaskCompletionSource<AlarmEventArgs>(TaskCreationOptions.RunContinuationsAsynchronously);
        drv.OnAlarmEvent += (_, e) =>
        {
            if (e.Message.Contains("raised")) raised.TrySetResult(e);
        };

        var sub = await drv.SubscribeAlarmsAsync(
            ["HighTempAlarm"], TestContext.Current.CancellationToken);

        // Pulse the input bit the ladder watches, then wait for the driver's poll loop
        // to see InFaulted rise + fire the raise event.
        _ = await drv.WriteAsync(
            [new WriteRequest("SimulateAlarm", true)],
            TestContext.Current.CancellationToken);

        var got = await Task.WhenAny(raised.Task, Task.Delay(TimeSpan.FromSeconds(5)));
        got.ShouldBe(raised.Task, "driver must surface the ALMD raise within 5 s of the ladder-driven edge");
        var args = await raised.Task;
        args.SourceNodeId.ShouldBe("HighTempAlarm");
        args.AlarmType.ShouldBe("ALMD");

        await drv.UnsubscribeAlarmsAsync(sub, TestContext.Current.CancellationToken);

        // Reset the bit so the next test run starts from a known state.
        _ = await drv.WriteAsync(
            [new WriteRequest("SimulateAlarm", false)],
            TestContext.Current.CancellationToken);
    }
}
