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

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

/// <summary>
///     Unit tests for <see cref="GalaxyDriverProbe"/>. The probe does a TCP preflight followed by
///     a lightweight gRPC ping (the read-only <c>GalaxyRepository.TestConnection</c> RPC) against
///     the mxaccessgw, classifying the resulting <see cref="StatusCode"/>. An auth rejection
///     (<see cref="StatusCode.Unauthenticated"/> / <see cref="StatusCode.PermissionDenied"/>)
///     counts as reachable because it proves a live gRPC server answered. The
///     <see cref="GalaxyDriverProbe.ClassifyRpc"/> mapping is unit-tested directly here without a
///     live gateway; the happy <c>OK</c> + auth-rejected paths are verified live against the real
///     gateway (10.100.0.48:5120).
/// </summary>
[Trait("Category", "Unit")]
public sealed class GalaxyDriverProbeTests
{
    private readonly GalaxyDriverProbe _probe = new();

    // A complete-enough GalaxyDriverOptions JSON skeleton; the {0} placeholder is the
    // gateway endpoint. All other nested records carry their required positional members.
    private static string ConfigJson(string endpoint, bool useTls = false) =>
        $$"""
        {
          "Gateway": {
            "Endpoint": "{{endpoint}}",
            "ApiKeySecretRef": "env:GALAXY_MXGW_API_KEY",
            "UseTls": {{(useTls ? "true" : "false")}}
          },
          "MxAccess": { "ClientName": "probe-test" },
          "Repository": {},
          "Reconnect": {}
        }
        """;

    // ── 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(2),
            TestContext.Current.CancellationToken);

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

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

    /// <summary>
    ///     Config JSON whose Gateway.Endpoint is empty returns Ok=false with a message
    ///     indicating no host/port was found.
    /// </summary>
    [Fact]
    public async Task NoEndpoint_returns_false_with_no_host_port_message()
    {
        var result = await _probe.ProbeAsync(
            ConfigJson(endpoint: ""),
            TimeSpan.FromSeconds(2),
            TestContext.Current.CancellationToken);

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

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

    /// <summary>
    ///     Pointing at a TCP port that is not open returns Ok=false. Depending on how far the
    ///     probe gets, the message is the TCP "Connect failed", a gRPC "handshake failed", or a
    ///     "timed out" message — all of which are acceptable failure signals for an unreachable
    ///     gateway.
    /// </summary>
    [Fact]
    public async Task ClosedPort_returns_false()
    {
        // Bind a port, capture it, then release it so nothing is listening when we probe.
        var listener = new TcpListener(IPAddress.Loopback, 0);
        listener.Start();
        var port = ((IPEndPoint)listener.LocalEndpoint).Port;
        listener.Stop();

        var endpoint = $"http://127.0.0.1:{port}";

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

        result.Ok.ShouldBeFalse();
        result.Message.ShouldNotBeNull();
        result.Message!.ShouldSatisfyAllConditions(
            () => (result.Message.Contains("Connect failed", StringComparison.OrdinalIgnoreCase)
                   || result.Message.Contains("handshake failed", StringComparison.OrdinalIgnoreCase)
                   || result.Message.Contains("timed out", StringComparison.OrdinalIgnoreCase))
                  .ShouldBeTrue($"unexpected message: '{result.Message}'"));
    }

    // ── 4. ClassifyRpc mapping (pure, no live gateway) ───────────────────────────

    /// <summary>A gRPC <c>OK</c> status maps to a reachable, gateway-confirmed result.</summary>
    [Fact]
    public void ClassifyRpc_Ok_is_reachable()
    {
        var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.OK, "host", 5120);
        ok.ShouldBeTrue();
        message.ShouldBe("gateway gRPC OK");
    }

    /// <summary>
    ///     A gRPC <c>Unauthenticated</c> status counts as reachable — an auth rejection proves a
    ///     live gateway gRPC server answered.
    /// </summary>
    [Fact]
    public void ClassifyRpc_Unauthenticated_is_reachable_auth_not_checked()
    {
        var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.Unauthenticated, "host", 5120);
        ok.ShouldBeTrue();
        message.ShouldContain("auth not checked", Case.Insensitive);
    }

    /// <summary>A gRPC <c>PermissionDenied</c> status also counts as reachable.</summary>
    [Fact]
    public void ClassifyRpc_PermissionDenied_is_reachable_auth_not_checked()
    {
        var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.PermissionDenied, "host", 5120);
        ok.ShouldBeTrue();
        message.ShouldContain("auth not checked", Case.Insensitive);
    }

    /// <summary>
    ///     A gRPC <c>Unavailable</c> status is a transport failure — Ok=false with a message that
    ///     names the host, port and the failing status code.
    /// </summary>
    [Fact]
    public void ClassifyRpc_Unavailable_is_not_reachable()
    {
        var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.Unavailable, "host", 5120);
        ok.ShouldBeFalse();
        message.ShouldContain("Unavailable");
        message.ShouldContain("host:5120");
    }

    /// <summary>A gRPC <c>DeadlineExceeded</c> status is likewise a handshake failure.</summary>
    [Fact]
    public void ClassifyRpc_DeadlineExceeded_is_not_reachable()
    {
        var (ok, message) = GalaxyDriverProbe.ClassifyRpc(StatusCode.DeadlineExceeded, "host", 5120);
        ok.ShouldBeFalse();
        message.ShouldContain("DeadlineExceeded");
        message.ShouldContain("host:5120");
    }
}
