using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient;
using ZB.MOM.WW.OtOpcUa.Driver.S7;

namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests;

/// <summary>
///     Phase 5 live verification that the real protocol-handshake Test-Connect probes actually
///     discriminate a speaking device from a merely-TCP-reachable one. Each probe is exercised
///     DIRECTLY (no cluster harness / SQL needed) against the shared docker-host sims, skip-gated
///     on reachability so <c>dotnet test</c> stays clean on a machine without fixture access.
///
///     <para>The decisive assertions are the cross-protocol RED cases: pointing a probe at a
///     DIFFERENT protocol's open port (which accepts TCP but does not speak the probe's protocol)
///     must now read <c>Ok = false</c> — the exact false-green bug Phase 5 fixes. Before Phase 5
///     every one of these read a false-healthy green.</para>
///
///     <para>S7 (<c>:1102</c>) and AbCip (<c>:44818</c>) happy-path verification skips unless those
///     fixtures are up (<c>lmxopcua-fix up s7 s7_1500</c> / <c>up abcip controllogix</c>); they are
///     unit-proven + code-reviewed. AbLegacy / TwinCAT / FOCAS have no rig target and are
///     unit-proven + degrade-guarded only (see <c>docs/drivers/TestConnectProbes.md</c>).</para>
/// </summary>
[Trait("Category", "Integration")]
[Trait("Phase", "5-probes")]
public sealed class DriverProbeHandshakeE2eTests
{
    private const string DockerHost = "10.100.0.35";
    private const int ModbusPort = 5020;     // pymodbus sim — speaks Modbus
    private const int OpcUaPort = 50000;     // opc-plc — speaks OPC UA
    private const int S7Port = 1102;
    private const int AbCipPort = 44818;
    private const string GalaxyHost = "10.100.0.48";
    private const int GalaxyPort = 5120;     // mxaccessgw — speaks gRPC

    // Local docker-dev rig (on the dev host): a REAL OPC UA server + a real non-OPC-UA server.
    private const int LocalOpcUaPort = 4840;  // central-1 OtOpcUa OPC UA server — speaks OPC UA
    private const int LocalSqlPort = 14330;   // SQL Server — accepts TCP, speaks neither OPC UA nor gRPC

    private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(10);
    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    private static void SkipUnless(string host, int port)
    {
        // Generous timeout: the first connect from a cold test process (JIT + DNS warmup) can
        // exceed the 500 ms default, and these targets may be a VPN hop away.
        if (!DockerFixtureAvailability.IsReachable(host, port, 3000))
            Assert.Skip($"Fixture {host}:{port} unreachable — skipping live handshake check.");
    }

    // ---- Modbus : FC03 handshake ----

    [Fact]
    public async Task Modbus_Green_AgainstModbusSim()
    {
        SkipUnless(DockerHost, ModbusPort);
        var result = await new ModbusDriverProbe().ProbeAsync(
            $"{{\"Host\":\"{DockerHost}\",\"Port\":{ModbusPort}}}", Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Message!.ShouldContain("Modbus FC03");
        result.Latency.ShouldNotBeNull();
    }

    [Fact]
    public async Task Modbus_Red_AgainstNonModbusPort()
    {
        // The OPC UA port accepts TCP but does not speak Modbus — must NOT read green.
        SkipUnless(DockerHost, OpcUaPort);
        var result = await new ModbusDriverProbe().ProbeAsync(
            $"{{\"Host\":\"{DockerHost}\",\"Port\":{OpcUaPort}}}", Timeout, Ct);
        result.Ok.ShouldBeFalse("A non-Modbus TCP server must not pass the FC03 handshake.");
    }

    // ---- OpcUaClient : GetEndpoints handshake ----

    [Fact]
    public async Task OpcUaClient_Green_AgainstOpcPlc()
    {
        SkipUnless(DockerHost, OpcUaPort);
        var result = await new OpcUaClientDriverProbe().ProbeAsync(
            $"{{\"EndpointUrl\":\"opc.tcp://{DockerHost}:{OpcUaPort}\"}}", Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Message!.ShouldContain("OPC UA");
        result.Latency.ShouldNotBeNull();
    }

    [Fact]
    public async Task OpcUaClient_Red_AgainstNonOpcUaPort()
    {
        // The Modbus port accepts TCP but does not speak OPC UA — must NOT read green.
        SkipUnless(DockerHost, ModbusPort);
        var result = await new OpcUaClientDriverProbe().ProbeAsync(
            $"{{\"EndpointUrl\":\"opc.tcp://{DockerHost}:{ModbusPort}\"}}", Timeout, Ct);
        result.Ok.ShouldBeFalse("A non-OPC-UA TCP server must not pass the GetEndpoints handshake.");
    }

    // ---- Galaxy : gRPC ping (auth-rejection = reachable) ----

    [Fact]
    public async Task Galaxy_Green_AgainstGateway()
    {
        SkipUnless(GalaxyHost, GalaxyPort);
        // No API key supplied — an Unauthenticated reply still proves a live mxaccessgw gRPC server.
        // UseTls:false matches the dev gateway's http2-cleartext endpoint (mirrors the dev config).
        var result = await new GalaxyDriverProbe().ProbeAsync(
            $"{{\"Gateway\":{{\"Endpoint\":\"http://{GalaxyHost}:{GalaxyPort}\",\"UseTls\":false}}}}", Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Latency.ShouldNotBeNull();
    }

    [Fact]
    public async Task Galaxy_Red_AgainstNonGrpcPort()
    {
        // The Modbus port accepts TCP but does not speak gRPC — must NOT read green.
        SkipUnless(DockerHost, ModbusPort);
        var result = await new GalaxyDriverProbe().ProbeAsync(
            $"{{\"Gateway\":{{\"Endpoint\":\"http://{DockerHost}:{ModbusPort}\",\"UseTls\":false}}}}", Timeout, Ct);
        result.Ok.ShouldBeFalse("A non-gRPC TCP server must not pass the gateway gRPC handshake.");
    }

    // ---- Local docker-dev rig: real OPC UA server (central-1) vs a real non-OPC-UA server ----

    [Fact]
    public async Task OpcUaClient_Green_AgainstLocalOtOpcUaServer()
    {
        SkipUnless("127.0.0.1", LocalOpcUaPort);
        var result = await new OpcUaClientDriverProbe().ProbeAsync(
            $"{{\"EndpointUrl\":\"opc.tcp://127.0.0.1:{LocalOpcUaPort}\"}}", Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Message!.ShouldContain("OPC UA");
        result.Latency.ShouldNotBeNull();
    }

    [Fact]
    public async Task OpcUaClient_Red_AgainstLocalNonOpcUaServer()
    {
        // SQL Server accepts TCP but does not speak OPC UA — the false-green bug Phase 5 fixes.
        SkipUnless("127.0.0.1", LocalSqlPort);
        var result = await new OpcUaClientDriverProbe().ProbeAsync(
            $"{{\"EndpointUrl\":\"opc.tcp://127.0.0.1:{LocalSqlPort}\"}}", Timeout, Ct);
        result.Ok.ShouldBeFalse("A SQL Server (non-OPC-UA) must not pass the GetEndpoints handshake.");
    }

    [Fact]
    public async Task Modbus_Red_AgainstLocalNonModbusServer()
    {
        // SQL Server accepts TCP but does not speak Modbus.
        SkipUnless("127.0.0.1", LocalSqlPort);
        var result = await new ModbusDriverProbe().ProbeAsync(
            $"{{\"Host\":\"127.0.0.1\",\"Port\":{LocalSqlPort}}}", Timeout, Ct);
        result.Ok.ShouldBeFalse("A SQL Server (non-Modbus) must not pass the FC03 handshake.");
    }

    // ---- S7 : Plc.OpenAsync handshake (skips unless the sim fixture is up) ----

    [Fact]
    public async Task S7_Green_AgainstSim()
    {
        SkipUnless(DockerHost, S7Port);
        var result = await new S7DriverProbe().ProbeAsync(
            $"{{\"Host\":\"{DockerHost}\",\"Port\":{S7Port},\"CpuType\":\"S71500\",\"Rack\":0,\"Slot\":1}}", Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Message!.ShouldContain("S7 connected");
    }

    // ---- AbCip : libplctag CIP session handshake (skips unless the sim fixture is up) ----

    [Fact]
    public async Task AbCip_Green_AgainstSim()
    {
        SkipUnless(DockerHost, AbCipPort);
        // Probe an explicit tag that the ab_server ControlLogix sim actually defines
        // (`TestDINT:DINT[1]`). The no-tags fallback (`@raw_cpu_type`) is NOT exercised here:
        // ab_server answers an unknown/unsupported tag with libplctag ErrorBadParam (a REAL
        // ControlLogix instead returns ErrorNotFound, which the probe classifies as
        // reachable). Whether the `@raw_cpu_type` system-tag fallback is valid on a real
        // ControlLogix is a hardware-gated follow-up (AbCipDriverOptions.cs flags it deferred).
        var result = await new AbCipDriverProbe().ProbeAsync(
            $"{{\"Devices\":[{{\"HostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\"}}]," +
            $"\"Tags\":[{{\"DeviceHostAddress\":\"ab://{DockerHost}:{AbCipPort}/1,0\",\"TagPath\":\"TestDINT\"}}]}}",
            Timeout, Ct);
        result.Ok.ShouldBeTrue($"Probe message: {result.Message}");
        result.Message!.ShouldContain("CIP session OK");
    }
}
