using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Nexus.Api.Controllers;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public class GatewayConnectorTests
{
    // ── Options / Configuration tests ──

    [Fact]
    public void Options_DefaultWebSocketPath_IsGatewayRoot()
    {
        var options = new GatewayConnectorOptions();
        Assert.Equal("/", options.WebSocketPath);
        Assert.Equal(4, options.ProtocolVersion);
        Assert.Equal(["operator.read"], options.Scopes);
        Assert.Equal("nexus", options.ClientId);
        Assert.Equal("backend", options.ClientMode);
        Assert.False(options.ExternalClientIdentitySupported);
        Assert.False(options.AllowReservedInternalClientIdentity);
    }

    [Fact]
    public void Options_DefaultReconnectInitialDelay_Is1000ms()
    {
        var options = new GatewayConnectorOptions();
        Assert.Equal(1000, options.ReconnectInitialDelayMs);
    }

    [Fact]
    public void Options_DefaultReconnectMaxDelay_Is5Minutes()
    {
        var options = new GatewayConnectorOptions();
        Assert.Equal(300_000, options.ReconnectMaxDelayMs);
    }

    [Fact]
    public void Options_FailFastOnVersionMismatch_IsTrueByDefault()
    {
        var options = new GatewayConnectorOptions();
        Assert.True(options.FailFastOnVersionMismatch);
    }

    [Fact]
    public void Options_FailFastOnMissingVersion_IsFalseByDefault()
    {
        var options = new GatewayConnectorOptions();
        Assert.False(options.FailFastOnMissingVersion);
    }

    [Fact]
    public void Connector_DefaultsToVerifiedGatewayVersionPin()
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Integrations:OpenClaw:BaseUrl"] = "http://127.0.0.1:18789"
            })
            .Build();
        var connector = new GatewayConnector(
            configuration,
            Options.Create(new GatewayConnectorOptions()),
            NullLogger<GatewayConnector>.Instance);

        Assert.Equal(
            OpenClawGatewayProtocol.DefaultRequiredGatewayVersion,
            connector.RequiredVersion);
        Assert.Equal("2026.7.1", connector.RequiredVersion);
    }

    // ── Configuration binding tests ──

    [Fact]
    public void Options_BindFromConfiguration()
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["GatewayConnector:WebSocketPath"] = "/events",
                ["GatewayConnector:ProtocolVersion"] = "4",
                ["GatewayConnector:ReconnectInitialDelayMs"] = "2000",
                ["GatewayConnector:ReconnectMaxDelayMs"] = "60000",
                ["GatewayConnector:FailFastOnVersionMismatch"] = "false",
                ["GatewayConnector:FailFastOnMissingVersion"] = "true"
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(config);
        services.AddOptions<GatewayConnectorOptions>()
            .BindConfiguration(GatewayConnectorOptions.SectionName);
        var sp = services.BuildServiceProvider();
        var options = sp.GetRequiredService<IOptions<GatewayConnectorOptions>>().Value;

        Assert.Equal("/events", options.WebSocketPath);
        Assert.Equal(4, options.ProtocolVersion);
        Assert.Equal(2000, options.ReconnectInitialDelayMs);
        Assert.Equal(60000, options.ReconnectMaxDelayMs);
        Assert.False(options.FailFastOnVersionMismatch);
        Assert.True(options.FailFastOnMissingVersion);
    }

    // ── Health endpoint tests ──

    [Fact]
    public async Task HealthEndpoint_ReturnsCorrectStructure()
    {
        var controller = new GatewayHealthController(new FakeGatewayConnector(
            GatewayConnectionState.Connected, "2.103.0", "2.103.0",
            DateTimeOffset.UtcNow, 0, "Gateway verbunden"));

        var body = await ExecuteAndReadJsonAsync(controller.GetGatewayHealth());

        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        Assert.Equal("connected", root.GetProperty("status").GetString());
        Assert.True(root.GetProperty("connected").GetBoolean());
        Assert.Equal("2.103.0", root.GetProperty("gatewayVersion").GetString());
        Assert.Equal("2.103.0", root.GetProperty("requiredVersion").GetString());
        Assert.True(root.GetProperty("versionPinned").GetBoolean());
        Assert.Equal(0, root.GetProperty("reconnectAttempts").GetInt32());
    }

    [Fact]
    public async Task HealthEndpoint_Disconnected_ShowsDisconnected()
    {
        var controller = new GatewayHealthController(new FakeGatewayConnector(
            GatewayConnectionState.Disconnected, null, "2.103.0",
            null, 5, "Connection refused"));

        var body = await ExecuteAndReadJsonAsync(controller.GetGatewayHealth());

        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        Assert.Equal("disconnected", root.GetProperty("status").GetString());
        Assert.False(root.GetProperty("connected").GetBoolean());
        Assert.Equal("unknown", root.GetProperty("gatewayVersion").GetString());
        Assert.Equal(5, root.GetProperty("reconnectAttempts").GetInt32());
        Assert.Equal("Connection refused", root.GetProperty("message").GetString());
    }

    [Fact]
    public async Task HealthEndpoint_Reconnecting_ShowsReconnecting()
    {
        var controller = new GatewayHealthController(new FakeGatewayConnector(
            GatewayConnectionState.Reconnecting, null, null,
            null, 3, "Reconnecting..."));

        var body = await ExecuteAndReadJsonAsync(controller.GetGatewayHealth());

        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        Assert.Equal("reconnecting", root.GetProperty("status").GetString());
        Assert.False(root.GetProperty("connected").GetBoolean());
        Assert.False(root.GetProperty("versionPinned").GetBoolean());
    }

    [Fact]
    public async Task HealthEndpoint_Failed_ShowsFailed()
    {
        var controller = new GatewayHealthController(new FakeGatewayConnector(
            GatewayConnectionState.Failed, "2.100.0", "2.103.0",
            null, 10, "Version mismatch — fail-fast"));

        var body = await ExecuteAndReadJsonAsync(controller.GetGatewayHealth());

        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        Assert.Equal("failed", root.GetProperty("status").GetString());
        Assert.False(root.GetProperty("connected").GetBoolean());
        Assert.Equal("2.100.0", root.GetProperty("gatewayVersion").GetString());
        Assert.Equal("2.103.0", root.GetProperty("requiredVersion").GetString());
        Assert.Equal(10, root.GetProperty("reconnectAttempts").GetInt32());
        Assert.True(root.TryGetProperty("timestamp", out _));
    }

    [Fact]
    public async Task HealthEndpoint_Unpinned_ShowsVersionPinnedFalse()
    {
        var controller = new GatewayHealthController(new FakeGatewayConnector(
            GatewayConnectionState.Connected, "2.103.0", null,
            DateTimeOffset.UtcNow, 0, null));

        var body = await ExecuteAndReadJsonAsync(controller.GetGatewayHealth());

        using var doc = JsonDocument.Parse(body);
        var root = doc.RootElement;

        Assert.False(root.GetProperty("versionPinned").GetBoolean());
        Assert.Equal(JsonValueKind.Null, root.GetProperty("requiredVersion").ValueKind);
    }

    // ── Connection state transition tests ──

    [Fact]
    public void Connector_InitialState_IsInitializing()
    {
        var connector = new FakeGatewayConnector(
            GatewayConnectionState.Initializing, null, null, null, 0, null);

        Assert.Equal(GatewayConnectionState.Initializing, connector.ConnectionState);
        Assert.Null(connector.GatewayVersion);
        Assert.Null(connector.LastConnectedAt);
        Assert.Equal(0, connector.ReconnectAttempts);
    }

    [Fact]
    public void Connector_AfterConnection_ReportsConnected()
    {
        var now = DateTimeOffset.UtcNow;
        var connector = new FakeGatewayConnector(
            GatewayConnectionState.Connected, "2.103.0", "2.103.0",
            now, 0, null);

        Assert.Equal(GatewayConnectionState.Connected, connector.ConnectionState);
        Assert.Equal("2.103.0", connector.GatewayVersion);
        Assert.Equal("2.103.0", connector.RequiredVersion);
        Assert.NotNull(connector.LastConnectedAt);
        Assert.Equal(0, connector.ReconnectAttempts);
    }

    // ── DI registration tests ──

    [Fact]
    public void Connector_CanBeRegisteredInDi()
    {
        var config = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Integrations:OpenClaw:BaseUrl"] = "http://localhost:18789",
                ["Integrations:OpenClaw:RequiredVersion"] = "2.103.0"
            })
            .Build();

        var services = new ServiceCollection();
        services.AddSingleton<IConfiguration>(config);
        services.AddLogging();
        services.AddHttpClient("gateway");
        services.AddOptions<GatewayConnectorOptions>()
            .BindConfiguration(GatewayConnectorOptions.SectionName);
        services.AddSingleton<IGatewayConnector, GatewayConnector>();

        var sp = services.BuildServiceProvider();
        var connector = sp.GetRequiredService<IGatewayConnector>();

        Assert.NotNull(connector);
        Assert.Equal(GatewayConnectionState.Initializing, connector.ConnectionState);
    }

    // ── HTTP endpoint routing test ──

    [Fact]
    public void HealthEndpoint_HasAllowAnonymous()
    {
        var type = typeof(GatewayHealthController);
        var attr = type.GetCustomAttributes(true)
            .OfType<Microsoft.AspNetCore.Authorization.AllowAnonymousAttribute>()
            .FirstOrDefault();
        Assert.NotNull(attr);
    }

    [Fact]
    public void HealthEndpoint_HasCorrectRoute()
    {
        var method = typeof(GatewayHealthController).GetMethod("GetGatewayHealth");
        Assert.NotNull(method);
        var routeAttr = method!.GetCustomAttributes(true)
            .OfType<Microsoft.AspNetCore.Mvc.HttpGetAttribute>()
            .FirstOrDefault();
        Assert.NotNull(routeAttr);
        Assert.Contains("/api/health/gateway", routeAttr!.Template!);
    }

    // ── Helpers ──

    private static async Task<string> ExecuteAndReadJsonAsync(IResult result)
    {
        var services = new ServiceCollection();
        services.AddLogging();
        services.ConfigureHttpJsonOptions(options =>
            options.SerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase);
        var sp = services.BuildServiceProvider();

        var httpContext = new DefaultHttpContext { RequestServices = sp };
        // HttpResults.Ok<T> needs a response body to write to
        httpContext.Response.Body = new MemoryStream();
        await result.ExecuteAsync(httpContext);
        httpContext.Response.Body.Position = 0;
        using var reader = new StreamReader(httpContext.Response.Body);
        return reader.ReadToEnd();
    }
}

/// <summary>
/// Test double implementing all members of IGatewayConnector.
/// </summary>
public sealed class FakeGatewayConnector : IGatewayConnector
{
    private readonly GatewayConnectionState _state;
    private readonly string? _gatewayVersion;
    private readonly string? _requiredVersion;
    private readonly DateTimeOffset? _lastConnectedAt;
    private readonly int _reconnectAttempts;
    private readonly string? _statusMessage;

    public FakeGatewayConnector(
        GatewayConnectionState state,
        string? gatewayVersion,
        string? requiredVersion,
        DateTimeOffset? lastConnectedAt,
        int reconnectAttempts,
        string? statusMessage)
    {
        _state = state;
        _gatewayVersion = gatewayVersion;
        _requiredVersion = requiredVersion;
        _lastConnectedAt = lastConnectedAt;
        _reconnectAttempts = reconnectAttempts;
        _statusMessage = statusMessage;
    }

    public GatewayConnectionState ConnectionState => _state;
    public string? GatewayVersion => _gatewayVersion;
    public string? RequiredVersion => _requiredVersion;
    public DateTimeOffset? LastConnectedAt => _lastConnectedAt;
    public int ReconnectAttempts => _reconnectAttempts;
    public string? StatusMessage => _statusMessage;
    public string? DeviceId => null;
    public bool DeviceTokenConfigured => false;
    public bool PairingRequired => false;
    public string? PairingRequestId => null;
    public int? ProtocolVersion => _state == GatewayConnectionState.Connected ? 4 : null;
    public IReadOnlySet<string> AdvertisedMethods { get; } = new HashSet<string>(StringComparer.Ordinal)
    {
        "health",
        "tasks.list"
    };
    public IReadOnlySet<string> AdvertisedEvents { get; } = new HashSet<string>(StringComparer.Ordinal)
    {
        "health",
        "tick"
    };
    public IReadOnlySet<string> GrantedScopes { get; } = new HashSet<string>(StringComparer.Ordinal)
    {
        "operator.read"
    };
    public DateTimeOffset? LastEventAt => null;

    public bool Supports(string method) => AdvertisedMethods.Contains(method);

    public Task<JsonNode?> InvokeAsync(
        string method,
        object? parameters = null,
        TimeSpan? timeout = null,
        CancellationToken cancellationToken = default,
        OpenClawInvocationContext? invocationContext = null)
        => Task.FromResult<JsonNode?>(null);

    public IReadOnlyList<GatewayEventEnvelope> GetRecentEvents(int limit = 100) => [];
}
