using Microsoft.Extensions.DependencyInjection;
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Commons.Interfaces;
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Admin;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;

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

/// <summary>
///     E2E integration coverage for the <c>TestDriverConnect</c> /
///     <see cref="TestDriverConnectResult"/> round-trip through the
///     <c>AdminOperationsActor</c> cluster singleton.
///
///     <para>All three tests target the Modbus Docker fixture that ships with the
///     <c>lmxopcua-fix up modbus</c> profile (default endpoint
///     <c>10.100.0.35:5020</c> from <c>MODBUS_SIM_ENDPOINT</c>). They are skipped
///     automatically when the fixture is unreachable so <c>dotnet test</c> on a dev
///     machine without Docker access still exits clean.</para>
/// </summary>
[Trait("Category", "Integration")]
[Trait("Driver", "Modbus")]
public sealed class DriverTestConnectE2eTests
{
    private const string DefaultEndpoint = "10.100.0.35:5020";
    private const string EndpointEnvVar = "MODBUS_SIM_ENDPOINT";

    private static CancellationToken Ct => TestContext.Current.CancellationToken;

    /// <summary>
    ///     Resolves the Modbus sim endpoint from the environment (or falls back to the shared
    ///     Docker host default) and returns host + port separately.
    /// </summary>
    private static (string host, int port) ResolveSimEndpoint()
    {
        var raw = Environment.GetEnvironmentVariable(EndpointEnvVar) ?? DefaultEndpoint;
        var parts = raw.Split(':', 2);
        var host = parts[0];
        var port = parts.Length == 2 && int.TryParse(parts[1], out var p) ? p : 5020;
        return (host, port);
    }

    /// <summary>
    ///     Happy-path probe: connects to the running Modbus pymodbus simulator and asserts
    ///     the <see cref="TestDriverConnectResult"/> reports <c>Ok = true</c> with a
    ///     sub-5 s latency. Skipped when the Docker fixture host is unreachable.
    /// </summary>
    [Fact]
    public async Task TestConnect_Modbus_AgainstFixture_ReportsOk()
    {
        var (host, port) = ResolveSimEndpoint();
        if (!DockerFixtureAvailability.IsReachable(host, port))
            Assert.Skip($"Modbus fixture at {host}:{port} unreachable — start with `lmxopcua-fix up modbus standard`.");

        await using var harness = await TwoNodeClusterHarness.StartAsync();
        await using var scope = harness.NodeA.Services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IAdminOperationsClient>();

        var configJson = $"{{\"Host\":\"{host}\",\"Port\":{port}}}";
        var correlationId = Guid.NewGuid();
        var msg = new TestDriverConnect("Modbus", configJson, TimeoutSeconds: 10, correlationId);

        var result = await client.AskAsync<TestDriverConnectResult>(msg, Ct);

        result.CorrelationId.ShouldBe(correlationId);
        result.Ok.ShouldBeTrue($"Probe reported failure: {result.Message}");
        result.LatencyMs.ShouldNotBeNull();
        result.LatencyMs!.Value.ShouldBeLessThan(5_000);
    }

    /// <summary>
    ///     Wrong-port probe: connects to the Docker fixture host on port 9999 (nothing
    ///     listens there) and asserts the result is <c>Ok = false</c> with a message
    ///     containing a connection-refused indicator. Skipped when the host is unreachable
    ///     (even a refused connection requires the IP to be routable).
    /// </summary>
    [Fact]
    public async Task TestConnect_Modbus_AgainstWrongPort_ReportsFailure()
    {
        var (host, _) = ResolveSimEndpoint();
        // Reachability check on the *correct* port to confirm the host is routable.
        var (_, goodPort) = ResolveSimEndpoint();
        if (!DockerFixtureAvailability.IsReachable(host, goodPort))
            Assert.Skip($"Modbus fixture host {host} not routable — cannot exercise refused-connection scenario.");

        await using var harness = await TwoNodeClusterHarness.StartAsync();
        await using var scope = harness.NodeA.Services.CreateAsyncScope();
        var client = scope.ServiceProvider.GetRequiredService<IAdminOperationsClient>();

        var configJson = $"{{\"Host\":\"{host}\",\"Port\":9999}}";
        var correlationId = Guid.NewGuid();
        var msg = new TestDriverConnect("Modbus", configJson, TimeoutSeconds: 5, correlationId);

        var result = await client.AskAsync<TestDriverConnectResult>(msg, Ct);

        result.CorrelationId.ShouldBe(correlationId);
        result.Ok.ShouldBeFalse("Port 9999 should not be open.");
        result.Message.ShouldNotBeNull();
        // SocketErrorCode is ConnectionRefused on Windows/Linux; on macOS it may appear as
        // "Connection refused" in the message text rather than the enum name.
        var failureMessage = result.Message!;
        (failureMessage.Contains("ConnectionRefused", StringComparison.OrdinalIgnoreCase)
          || failureMessage.Contains("refused", StringComparison.OrdinalIgnoreCase)
          || failureMessage.Contains("Connect failed", StringComparison.OrdinalIgnoreCase))
            .ShouldBeTrue($"Expected a refused-connection message but got: {failureMessage}");
    }

    /// <summary>
    ///     Black-hole probe: targets <c>1.2.3.4:502</c> (TEST-NET-1, packets are dropped).
    ///     Uses a 3-second timeout to keep the wall-clock cost low. Asserts the probe
    ///     reports <c>Ok = false</c> with a message containing "timed out".
    ///
    ///     <para><b>Scope:</b> the <see cref="TwoNodeClusterHarness"/> does not register
    ///     <c>IDriverProbe</c> implementations (those are wired by
    ///     <c>DriverFactoryBootstrap</c> in Program.cs). This test therefore exercises
    ///     <see cref="ModbusDriverProbe.ProbeAsync"/> directly rather than going through
    ///     the <c>AdminOperationsActor</c>. The cluster round-trip path is covered by
    ///     <see cref="TestConnect_Modbus_AgainstFixture_ReportsOk"/> (which skips in dev).
    ///     This test does NOT require any Docker fixture and always runs.</para>
    /// </summary>
    [Fact]
    public async Task TestConnect_Modbus_AgainstBlackHole_ReportsTimeout()
    {
        var probe = new ModbusDriverProbe();

        // 1.2.3.4 is TEST-NET-1 (RFC 5737) — routable but black-holed; packets never return.
        const string configJson = "{\"Host\":\"1.2.3.4\",\"Port\":502}";
        var timeout = TimeSpan.FromSeconds(3);
        using var cts = new CancellationTokenSource(timeout);

        var result = await probe.ProbeAsync(configJson, timeout, cts.Token);

        result.Ok.ShouldBeFalse("TEST-NET-1 connection should time out.");
        result.Message.ShouldNotBeNull();
        result.Message!.ToLowerInvariant().ShouldContain("timed out");
    }
}
