using System.Net;
using System.Net.Sockets;
using Shouldly;
using Xunit;

namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;

/// <summary>
///     Unit tests for <see cref="OpcUaClientDriverProbe"/>. Uses an in-process
///     <see cref="TcpListener"/> on 127.0.0.1 to exercise the accept-then-close negative path
///     (non-OPC-UA TCP server). The happy path (real OPC UA endpoints) is covered live.
/// </summary>
[Trait("Category", "Unit")]
public sealed class OpcUaClientDriverProbeTests
{
    private readonly OpcUaClientDriverProbe _probe = new();

    // ── 1. Invalid JSON ──────────────────────────────────────────────────────────

    /// <summary>
    ///     Invalid JSON returns Ok=false with a message containing "invalid".
    /// </summary>
    [Fact]
    public async Task InvalidJson_returns_false_with_invalid_message()
    {
        var result = await _probe.ProbeAsync(
            "not-json",
            TimeSpan.FromSeconds(3),
            TestContext.Current.CancellationToken);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message!.ShouldContain("invalid", Case.Insensitive);
    }

    // ── 2. Config with no endpoint URL ───────────────────────────────────────────

    /// <summary>
    ///     Config JSON that resolves to no endpoint URL returns Ok=false with a message
    ///     indicating no host/port was found.
    /// </summary>
    [Fact]
    public async Task NoEndpointUrl_returns_false_with_no_host_port_message()
    {
        // Empty EndpointUrl + empty EndpointUrls → ExtractTarget returns ("", 0, "")
        var result = await _probe.ProbeAsync(
            """{"EndpointUrl":"","EndpointUrls":[]}""",
            TimeSpan.FromSeconds(3),
            TestContext.Current.CancellationToken);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message!.ShouldContain("no host", Case.Insensitive);
    }

    // ── 3. Unreachable closed port ────────────────────────────────────────────────

    /// <summary>
    ///     Pointing at a TCP port that is not open returns Ok=false with a "Connect failed"
    ///     message from the socket layer.
    /// </summary>
    [Fact]
    public async Task ClosedPort_returns_false_with_connect_failed_message()
    {
        // Find a free port by letting the OS assign one, then immediately close the listener
        // so nothing is bound at that port when we probe it.
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();

        var configJson = $$"""{"EndpointUrl":"opc.tcp://127.0.0.1:{{port}}"}""";

        using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
        var result = await _probe.ProbeAsync(configJson, TimeSpan.FromSeconds(5), cts.Token);

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        // SocketException on a closed port emits "Connect failed: ConnectionRefused" (or similar).
        result.Message!.ShouldContain("Connect failed", Case.Insensitive);
    }

    // ── 4. TCP accept-then-close (non-OPC-UA server) ────────────────────────────

    /// <summary>
    ///     A TCP server that accepts but immediately closes the connection (simulating a
    ///     non-OPC-UA process on the target port) causes the GetEndpoints handshake to fail.
    ///     The result must be Ok=false and the message must contain "handshake failed".
    /// </summary>
    [Fact]
    public async Task NonOpcUaTcpServer_returns_false_with_handshake_failed_message()
    {
        // Start a listener that accepts the connection and immediately closes it, simulating
        // a non-OPC UA TCP service (e.g. an HTTP server or an SSH daemon) on the target port.
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;

        var testCt = TestContext.Current.CancellationToken;

        // Accept in background — immediately close the socket so the OPC UA client gets EOF.
        var acceptTask = Task.Run(async () =>
        {
            try
            {
                using var client = await listener.AcceptTcpClientAsync(testCt);
                // Close immediately — the OPC UA handshake will get an EOF / broken pipe.
            }
            catch
            {
                // Listener may be stopped before a second accept; ignore.
            }
            finally
            {
                listener.Stop();
            }
        }, testCt);

        try
        {
            var configJson = $$"""{"EndpointUrl":"opc.tcp://127.0.0.1:{{port}}"}""";

            // Use a generous timeout so the test is not fragile on a slow CI box, but short
            // enough to not block the suite for long if something goes wrong.
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
            var result = await _probe.ProbeAsync(configJson, TimeSpan.FromSeconds(10), cts.Token);

            result.Ok.ShouldBeFalse();
            result.Message.ShouldNotBeNull();
            result.Message!.ShouldContain("handshake failed", Case.Insensitive);
        }
        finally
        {
            await acceptTask.WaitAsync(TimeSpan.FromSeconds(5), testCt);
        }
    }
}
