using System.Text.Json.Nodes;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class OpenClawGatewayProtocolTests
{
    [Fact]
    public void ConnectFrame_UsesExplicitNexusIdentityAndRequestedScopes()
    {
        var options = new GatewayConnectorOptions
        {
            Scopes = ["operator.read", "operator.write"],
            Capabilities = ["session-scoped-events"]
        };

        var frame = OpenClawGatewayProtocol.BuildConnectRequest(
            "connect-1",
            options,
            token: "test-token",
            password: null,
            clientVersion: "1.2.3",
            platform: "windows",
            locale: "de-DE");

        Assert.Equal("req", frame["type"]!.GetValue<string>());
        Assert.Equal("connect", frame["method"]!.GetValue<string>());
        Assert.Equal("nexus", frame["params"]!["client"]!["id"]!.GetValue<string>());
        Assert.Equal("backend", frame["params"]!["client"]!["mode"]!.GetValue<string>());
        Assert.Equal("Nexus Mission Control", frame["params"]!["client"]!["displayName"]!.GetValue<string>());
        Assert.Equal(4, frame["params"]!["minProtocol"]!.GetValue<int>());
        Assert.Equal("test-token", frame["params"]!["auth"]!["token"]!.GetValue<string>());
        Assert.Null(frame["params"]!["auth"]!["password"]);
        Assert.Equal(2, frame["params"]!["scopes"]!.AsArray().Count);
    }

    [Fact]
    public void ClientIdentity_FailsClosedUntilExternalNexusIdentityIsSupported()
    {
        var exception = Assert.Throws<OpenClawGatewayRpcException>(() =>
            OpenClawGatewayProtocol.ValidateExternalClientIdentity(new GatewayConnectorOptions()));

        Assert.Equal("EXTERNAL_CLIENT_ID_UNSUPPORTED", exception.Code);
    }

    [Fact]
    public void ClientIdentity_RejectsReservedInternalGatewayIdentity()
    {
        var options = new GatewayConnectorOptions
        {
            ClientId = "gateway-client",
            ClientMode = "backend",
            ExternalClientIdentitySupported = true
        };

        var exception = Assert.Throws<OpenClawGatewayRpcException>(() =>
            OpenClawGatewayProtocol.ValidateExternalClientIdentity(options));

        Assert.Equal("RESERVED_CLIENT_ID", exception.Code);
    }

    [Fact]
    public void ClientIdentity_AcceptsNexusOnlyAfterExplicitContractSupport()
    {
        var options = new GatewayConnectorOptions
        {
            ExternalClientIdentitySupported = true
        };

        OpenClawGatewayProtocol.ValidateExternalClientIdentity(options);
    }

    [Fact]
    public void ConnectFrame_PrefersExplicitPasswordAuth()
    {
        var frame = OpenClawGatewayProtocol.BuildConnectRequest(
            "connect-2",
            new GatewayConnectorOptions(),
            token: "ignored-token",
            password: "test-password",
            clientVersion: "1.0.0",
            platform: "linux",
            locale: "en-US");

        Assert.Equal("test-password", frame["params"]!["auth"]!["password"]!.GetValue<string>());
        Assert.Null(frame["params"]!["auth"]!["token"]);
    }

    [Fact]
    public void ConnectFrame_IncludesChallengeBoundDeviceProofAndDeviceToken()
    {
        var proof = new OpenClawGatewayDeviceProof(
            "device-1",
            "public-key",
            "signature",
            1_737_264_000_000,
            "nonce-1");

        var frame = OpenClawGatewayProtocol.BuildConnectRequest(
            "connect-device",
            new GatewayConnectorOptions(),
            token: "device-token",
            password: null,
            clientVersion: "1.0.0",
            platform: "Linux",
            locale: "en-US",
            device: proof,
            scopes: ["operator.read"],
            deviceFamily: "Server",
            deviceToken: "device-token");

        var parameters = frame["params"]!;
        Assert.Equal("server", parameters["client"]!["deviceFamily"]!.GetValue<string>().ToLowerInvariant());
        Assert.Equal("device-1", parameters["device"]!["id"]!.GetValue<string>());
        Assert.Equal("nonce-1", parameters["device"]!["nonce"]!.GetValue<string>());
        Assert.Equal("device-token", parameters["auth"]!["token"]!.GetValue<string>());
        Assert.Equal("device-token", parameters["auth"]!["deviceToken"]!.GetValue<string>());
        Assert.Single(parameters["scopes"]!.AsArray());
    }

    [Fact]
    public void DevicePayloadV3_MatchesCanonicalOpenClawOrderingAndNormalization()
    {
        var payload = OpenClawGatewayProtocol.BuildDeviceAuthPayloadV3(
            "device-1",
            "gateway-client",
            "backend",
            "operator",
            ["operator.read", "operator.write"],
            1_737_264_000_000,
            "token-1",
            "nonce-1",
            "Windows",
            "Server");

        Assert.Equal(
            "v3|device-1|gateway-client|backend|operator|operator.read,operator.write|1737264000000|token-1|nonce-1|windows|server",
            payload);
    }

    [Theory]
    [InlineData("ws://127.0.0.1:18789", false)]
    [InlineData("ws://localhost:18789", false)]
    [InlineData("ws://[::1]:18789", false)]
    [InlineData("ws://host.docker.internal:18789", true)]
    [InlineData("wss://gateway.example.test", true)]
    public void DeviceIdentity_IsOmittedOnlyForDirectLoopback(
        string endpoint,
        bool expected)
    {
        Assert.Equal(
            expected,
            OpenClawGatewayProtocol.RequiresDeviceIdentity(new Uri(endpoint)));
    }

    [Fact]
    public void RpcFrame_PropagatesTraceparentAndSchemaConfirmedIdempotencyKey()
    {
        var context = OpenClawInvocationContext.Create(
            actor: "owner-1",
            idempotencyKey: "idem-1",
            correlationId: "corr-1",
            traceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
            includeIdempotencyParameter: true);

        var frame = OpenClawGatewayProtocol.BuildRpcRequest(
            "req-1",
            "chat.send",
            JsonNode.Parse("""{ "sessionKey": "agent:iris:main", "message": "hello" }"""),
            context);

        Assert.Equal(context.TraceParent, frame["traceparent"]!.GetValue<string>());
        Assert.Equal("idem-1", frame["params"]!["idempotencyKey"]!.GetValue<string>());
    }

    [Fact]
    public void RpcFrame_DoesNotInventIdempotencyFieldWithoutSchemaOptIn()
    {
        var context = OpenClawInvocationContext.Create(
            idempotencyKey: "idem-closed-schema",
            traceParent: "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01");

        var frame = OpenClawGatewayProtocol.BuildRpcRequest(
            "req-2",
            "tasks.cancel",
            JsonNode.Parse("""{ "taskId": "task-1" }"""),
            context);

        Assert.Null(frame["params"]!["idempotencyKey"]);
    }

    [Fact]
    public void RpcFrame_RejectsInvalidTraceparentBeforeSending()
    {
        var context = new OpenClawInvocationContext(
            "idem-1",
            "corr-1",
            "owner-1",
            "not-a-traceparent");

        Assert.Throws<ArgumentException>(() =>
            OpenClawGatewayProtocol.BuildRpcRequest(
                "req-3",
                "tasks.cancel",
                new JsonObject(),
                context));
    }

    [Fact]
    public void ParseHello_ProjectsProtocolFeaturesAndScopes()
    {
        var frame = JsonNode.Parse("""
            {
              "type": "res",
              "id": "connect-3",
              "ok": true,
              "payload": {
                "type": "hello-ok",
                "protocol": 4,
                "server": { "version": "2026.7.28", "connId": "conn-1" },
                "features": {
                  "methods": ["tasks.list", "sessions.list"],
                  "events": ["tick", "sessions.changed"]
                },
                "auth": {
                  "deviceToken": "paired-token",
                  "role": "operator",
                  "scopes": ["operator.read", "operator.write"]
                },
                "policy": {
                  "maxPayload": 26214400,
                  "maxBufferedBytes": 52428800,
                  "tickIntervalMs": 15000
                }
              }
            }
            """);

        var hello = OpenClawGatewayProtocol.ParseHello(frame, "connect-3");

        Assert.Equal(4, hello.Protocol);
        Assert.Equal("2026.7.28", hello.ServerVersion);
        Assert.Contains("tasks.list", hello.Methods);
        Assert.Contains("sessions.changed", hello.Events);
        Assert.Contains("operator.write", hello.Scopes);
        Assert.Equal(26_214_400, hello.MaxPayload);
        Assert.Equal("paired-token", hello.DeviceToken);
        Assert.Equal("operator", hello.Role);
    }

    [Fact]
    public void ParseHello_PreservesStructuredGatewayError()
    {
        var frame = JsonNode.Parse("""
            {
              "type": "res",
              "id": "connect-4",
              "ok": false,
              "error": {
                "code": "FORBIDDEN",
                "message": "missing scope",
                "retryable": false,
                "details": {
                  "code": "MISSING_SCOPE",
                  "missingScope": "operator.approvals"
                }
              }
            }
            """);

        var exception = Assert.Throws<OpenClawGatewayRpcException>(
            () => OpenClawGatewayProtocol.ParseHello(frame, "connect-4"));

        Assert.Equal("FORBIDDEN", exception.Code);
        Assert.Equal("MISSING_SCOPE", exception.Details!["code"]!.GetValue<string>());
        Assert.Equal("operator.approvals", exception.Details!["missingScope"]!.GetValue<string>());
    }

    [Fact]
    public void PairingError_PreservesExactRequestIdForOperatorApproval()
    {
        var exception = OpenClawGatewayProtocol.CreateRpcException(JsonNode.Parse(
            """
            {
              "code": "PAIRING_REQUIRED",
              "message": "pairing required",
              "retryable": true,
              "details": {
                "code": "PAIRING_REQUIRED",
                "requestId": "pair-request-42",
                "recommendedNextStep": "wait_then_retry"
              }
            }
            """));

        Assert.True(OpenClawGatewayProtocol.TryReadPairingRequest(exception, out var requestId));
        Assert.Equal("pair-request-42", requestId);
    }
}
