using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Nexus.Api.Controllers;
using Nexus.Api.Data;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class OpenClawSetupServiceTests
{
    [Fact]
    public void SetupController_IsOwnerOnly()
    {
        var authorize = Assert.Single(
            typeof(OpenClawSetupController)
                .GetCustomAttributes(typeof(AuthorizeAttribute), inherit: true)
                .OfType<AuthorizeAttribute>());

        Assert.Equal("owner", authorize.Roles);
        Assert.Empty(
            typeof(OpenClawSetupController)
                .GetMethods()
                .SelectMany(method =>
                    method.GetCustomAttributes(typeof(AllowAnonymousAttribute), inherit: true)));
    }

    [Fact]
    public async Task Discover_ReturnsOnlyConfiguredAndWellKnownCandidates_WithoutInvokingGateway()
    {
        var gateway = ConnectedGateway();
        var service = CreateService(gateway: gateway);

        var result = await service.DiscoverAsync(new OpenClawDiscoveryRequest(IncludeMdns: true));

        Assert.Equal("unsupported", result.MdnsState);
        Assert.Contains(result.Candidates, candidate =>
            candidate.Endpoint == "ws://openclaw-gateway:18789/"
            && candidate.IsValid);
        Assert.Contains(result.Candidates, candidate =>
            candidate.Endpoint == "ws://127.0.0.1:18789/"
            && candidate.IsCurrentConnectorEndpoint);
        Assert.DoesNotContain(result.Candidates, candidate =>
            candidate.Endpoint.Contains("0.0.0.0", StringComparison.Ordinal));
        Assert.Empty(gateway.Invocations);
    }

    [Theory]
    [InlineData("ws://example.com:18789/", null)]
    [InlineData("wss://user:secret@example.com/", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
    [InlineData("wss://example.com/?token=secret", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")]
    [InlineData("wss://example.com/", null)]
    public async Task Probe_RejectsUnsafeExternalEndpoints(
        string endpoint,
        string? fingerprint)
    {
        var service = CreateService();

        var result = await service.ProbeAsync(
            new ProbeOpenClawRequest(endpoint, fingerprint));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.InvalidEndpoint, result.State);
        Assert.Null(result.Data);
    }

    [Fact]
    public async Task Probe_ValidExternalEndpointStaysBlockedWithoutOfficialClientIdentity()
    {
        var gateway = ConnectedGateway();
        var service = CreateService(gateway: gateway);

        var result = await service.ProbeAsync(new ProbeOpenClawRequest(
            "wss://gateway.example.test/",
            "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.ExperimentalBlocked, result.State);
        Assert.NotNull(result.Data);
        Assert.Empty(gateway.Invocations);
        Assert.Empty(gateway.ConfiguredEndpoints);
    }

    [Fact]
    public async Task Probe_BlocksUnofficialClientIdentity_EvenWhenLegacyConnectorIsConnected()
    {
        var service = CreateService(gateway: ConnectedGateway());

        var result = await service.ProbeAsync(
            new ProbeOpenClawRequest("ws://127.0.0.1:18789/"));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.ExperimentalBlocked, result.State);
        Assert.False(result.Data?.CanAttach);
    }

    [Fact]
    public async Task Attach_UsesTransientBootstrapSecret_WithoutPersistingIt()
    {
        var repository = new FakeOpenClawConnectionProfileRepository();
        var gateway = ConnectedGateway();
        var service = CreateService(
            repository,
            gateway,
            externalIdentitySupported: true);
        var request = new AttachOpenClawRequest(
            "ws://127.0.0.1:18789/",
            "manual",
            BootstrapToken: "one-time-secret");

        var result = await service.AttachAsync(request);

        Assert.True(result.Ok);
        Assert.Equal(OpenClawSetupStates.Attached, result.State);
        Assert.NotNull(repository.Current);
        Assert.True(gateway.BootstrapTokenSupplied);
        Assert.DoesNotContain("one-time-secret", JsonSerializer.Serialize(repository.Current), StringComparison.Ordinal);
        Assert.DoesNotContain("one-time-secret", JsonSerializer.Serialize(result), StringComparison.Ordinal);
        Assert.DoesNotContain("one-time-secret", request.ToString(), StringComparison.Ordinal);
    }

    [Fact]
    public async Task Attach_PersistsOnlySecretFreeProfile_WhenSupportedReadOnlyConnectorIsReady()
    {
        var repository = new FakeOpenClawConnectionProfileRepository();
        var service = CreateService(
            repository,
            ConnectedGateway(),
            externalIdentitySupported: true);

        var result = await service.AttachAsync(new AttachOpenClawRequest(
            "ws://127.0.0.1:18789/",
            "manual"));

        Assert.True(result.Ok);
        Assert.Equal(OpenClawSetupStates.Attached, result.State);
        Assert.NotNull(repository.Current);
        Assert.Equal(OpenClawAdoptionStates.Attached, repository.Current!.AdoptionState);
        Assert.False(repository.Current.ManagementEnabled);
        Assert.Equal(1, repository.Current.Revision);
        var serialized = JsonSerializer.Serialize(repository.Current);
        Assert.DoesNotContain("BootstrapToken", serialized, StringComparison.Ordinal);
    }

    [Fact]
    public async Task Attach_RejectsLegacyAdminScopeBeforeAdoption()
    {
        var repository = new FakeOpenClawConnectionProfileRepository();
        var gateway = ConnectedGateway();
        gateway.GrantedScopes =
            new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
        var service = CreateService(
            repository,
            gateway,
            externalIdentitySupported: true);

        var result = await service.AttachAsync(new AttachOpenClawRequest(
            "ws://127.0.0.1:18789/",
            "configured"));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.ExcessiveScope, result.State);
        Assert.Null(repository.Current);
    }

    [Fact]
    public async Task Verify_DoesNotAdvanceAttachedProfileWithAdminScope()
    {
        var repository = new FakeOpenClawConnectionProfileRepository
        {
            Current = Profile(OpenClawAdoptionStates.Attached, revision: 1)
        };
        var gateway = ConnectedGateway();
        gateway.GrantedScopes =
            new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
        var service = CreateService(repository, gateway, externalIdentitySupported: true);

        var result = await service.VerifyAsync(new VerifyOpenClawRequest(1));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.ExcessiveScope, result.State);
        Assert.Equal(OpenClawAdoptionStates.Attached, repository.Current?.AdoptionState);
        Assert.Equal(1, repository.Current?.Revision);
    }

    [Fact]
    public async Task Adopt_CollectsLiveInventory_AndDoesNotCopyResourcePayloads()
    {
        var repository = new FakeOpenClawConnectionProfileRepository
        {
            Current = Profile(
                adoptionState: OpenClawAdoptionStates.Verified,
                revision: 3)
        };
        var gateway = ConnectedGateway(
            "agents.list",
            "agents.files.list",
            "cron.list",
            "models.list",
            "channels.status",
            "nodes.list");
        gateway.Handler = (method, parameters) => method switch
        {
            "agents.list" => JsonNode.Parse(
                """{"agents":[{"id":"iris"},{"id":"programmer"}]}"""),
            "agents.files.list" => JsonNode.Parse(
                """{"files":[{"name":"AGENTS.md"},{"name":"SOUL.md"}]}"""),
            "cron.list" => JsonNode.Parse(
                """{"jobs":[{},{},{},{},{},{},{}]}"""),
            "models.list" => JsonNode.Parse(
                """{"models":[{},{},{}]}"""),
            "channels.status" => JsonNode.Parse(
                """{"channels":{"telegram":{},"discord":{}}}"""),
            "nodes.list" => JsonNode.Parse(
                """{"nodes":[{}]}"""),
            _ => null
        };
        var service = CreateService(
            repository,
            gateway,
            externalIdentitySupported: true);

        var result = await service.AdoptAsync(new AdoptOpenClawRequest(3));

        Assert.True(result.Ok);
        Assert.Equal(2, result.Data?.AgentCount);
        Assert.Equal(4, result.Data?.AgentFileCount);
        Assert.Equal(7, result.Data?.CronJobCount);
        Assert.Equal(3, result.Data?.ModelCount);
        Assert.Equal(2, result.Data?.ChannelCount);
        Assert.Equal(1, result.Data?.NodeCount);
        Assert.Equal(OpenClawAdoptionStates.Adopted, repository.Current?.AdoptionState);
        Assert.False(repository.Current?.ManagementEnabled);
        Assert.Equal(4, repository.Current?.Revision);
    }

    [Fact]
    public async Task Management_RequiresExplicitAdminScopeUpgrade()
    {
        var repository = new FakeOpenClawConnectionProfileRepository
        {
            Current = Profile(
                adoptionState: OpenClawAdoptionStates.Adopted,
                revision: 4)
        };
        var gateway = ConnectedGateway();
        var service = CreateService(
            repository,
            gateway,
            externalIdentitySupported: true);

        var blocked = await service.SetManagementAsync(
            new SetOpenClawManagementRequest(true, true, 4));

        Assert.False(blocked.Ok);
        Assert.Equal(OpenClawSetupStates.ScopeUpgradeRequired, blocked.State);
        Assert.False(repository.Current?.ManagementEnabled);
        Assert.Contains(
            gateway.RequestedScopeSets,
            scopes => scopes.SetEquals(["operator.read", "operator.admin"]));

        gateway.GrantedScopes =
            new HashSet<string>(["operator.read", "operator.admin"], StringComparer.Ordinal);
        var enabled = await service.SetManagementAsync(
            new SetOpenClawManagementRequest(true, true, 4));

        Assert.True(enabled.Ok);
        Assert.True(repository.Current?.ManagementEnabled);
        Assert.Equal(5, repository.Current?.Revision);
    }

    [Fact]
    public async Task ProfileMutation_RejectsStaleRevision()
    {
        var repository = new FakeOpenClawConnectionProfileRepository
        {
            Current = Profile(
                adoptionState: OpenClawAdoptionStates.Adopted,
                revision: 7)
        };
        var service = CreateService(
            repository,
            ConnectedGateway(),
            externalIdentitySupported: true);

        var result = await service.SetManagementAsync(
            new SetOpenClawManagementRequest(false, true, 6));

        Assert.False(result.Ok);
        Assert.Equal(OpenClawSetupStates.ConcurrencyConflict, result.State);
        Assert.Equal(7, repository.Current?.Revision);
    }

    [Fact]
    public async Task Delete_RequiresExactEndpointAndDeviceConfirmation()
    {
        var repository = new FakeOpenClawConnectionProfileRepository
        {
            Current = Profile(
                adoptionState: OpenClawAdoptionStates.Adopted,
                revision: 2,
                deviceId: "device-bao")
        };
        var gateway = ConnectedGateway();
        var service = CreateService(repository, gateway);

        var rejected = await service.DeleteAsync(
            new DeleteOpenClawConnectionRequest(
                "ws://localhost:18789/",
                "device-bao",
                2));
        Assert.False(rejected.Ok);
        Assert.Equal(OpenClawSetupStates.InvalidRequest, rejected.State);
        Assert.NotNull(repository.Current);

        var removed = await service.DeleteAsync(
            new DeleteOpenClawConnectionRequest(
                "ws://127.0.0.1:18789/",
                "device-bao",
                2));
        Assert.True(removed.Ok);
        Assert.Equal(OpenClawSetupStates.Removed, removed.State);
        Assert.Null(repository.Current);
        Assert.True(gateway.DisconnectRequested);
    }

    private static OpenClawSetupService CreateService(
        FakeOpenClawConnectionProfileRepository? repository = null,
        SetupGatewayConnector? gateway = null,
        bool externalIdentitySupported = false)
    {
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["Integrations:OpenClaw:BaseUrl"] = "http://127.0.0.1:18789",
                ["Integrations:OpenClaw:RequiredVersion"] = "2026.7.1",
                ["GatewayConnector:WebSocketPath"] = "/",
                ["OpenClawSetup:ExternalClientIdentitySupported"] =
                    externalIdentitySupported.ToString()
            })
            .Build();
        return new OpenClawSetupService(
            repository ?? new FakeOpenClawConnectionProfileRepository(),
            gateway ?? ConnectedGateway(),
            configuration);
    }

    private static SetupGatewayConnector ConnectedGateway(params string[] additionalMethods)
    {
        var methods = new HashSet<string>(additionalMethods, StringComparer.Ordinal);
        return new SetupGatewayConnector
        {
            ConnectionState = GatewayConnectionState.Connected,
            GatewayVersion = "2026.7.1",
            RequiredVersion = "2026.7.1",
            ProtocolVersion = 4,
            DeviceId = "device-bao",
            DeviceTokenConfigured = true,
            ActiveEndpoint = "ws://127.0.0.1:18789/",
            GrantedScopes = new HashSet<string>(["operator.read"], StringComparer.Ordinal),
            AdvertisedMethods = methods
        };
    }

    private static OpenClawConnectionProfile Profile(
        string adoptionState,
        int revision,
        string? deviceId = "device-bao")
        => new()
        {
            Endpoint = "ws://127.0.0.1:18789/",
            DiscoverySource = "configured",
            RequiredVersion = "2026.7.1",
            AdoptionState = adoptionState,
            ManagementEnabled = false,
            CapabilityHash = new string('a', 64),
            DeviceId = deviceId,
            Revision = revision,
            CreatedAt = DateTimeOffset.UtcNow.AddMinutes(-5),
            UpdatedAt = DateTimeOffset.UtcNow
        };
}

internal sealed class FakeOpenClawConnectionProfileRepository
    : IOpenClawConnectionProfileRepository
{
    public OpenClawConnectionProfile? Current { get; set; }

    public Task<OpenClawConnectionProfile?> GetPrimaryAsync(
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        return Task.FromResult(Clone(Current));
    }

    public Task<OpenClawConnectionProfile> SavePrimaryAsync(
        OpenClawConnectionProfile profile,
        int? expectedRevision,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (Current is null)
        {
            if (expectedRevision is not null and not 0)
                throw new OpenClawConnectionProfileConcurrencyException("Profile missing.");
            Current = Clone(profile)!;
            Current.Revision = 1;
        }
        else
        {
            if (expectedRevision != Current.Revision)
            {
                throw new OpenClawConnectionProfileConcurrencyException(
                    "Profile changed.",
                    Current.Revision);
            }

            Current = Clone(profile)!;
            Current.Revision = expectedRevision.Value + 1;
        }

        Current.UpdatedAt = DateTimeOffset.UtcNow;
        return Task.FromResult(Clone(Current)!);
    }

    public Task<bool> DeletePrimaryAsync(
        int expectedRevision,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (Current is null)
            return Task.FromResult(false);
        if (Current.Revision != expectedRevision)
        {
            throw new OpenClawConnectionProfileConcurrencyException(
                "Profile changed.",
                Current.Revision);
        }

        Current = null;
        return Task.FromResult(true);
    }

    private static OpenClawConnectionProfile? Clone(OpenClawConnectionProfile? profile)
        => profile is null
            ? null
            : new OpenClawConnectionProfile
            {
                ProfileId = profile.ProfileId,
                Endpoint = profile.Endpoint,
                DiscoverySource = profile.DiscoverySource,
                RequiredVersion = profile.RequiredVersion,
                TlsCertificateFingerprint = profile.TlsCertificateFingerprint,
                AdoptionState = profile.AdoptionState,
                ManagementEnabled = profile.ManagementEnabled,
                CapabilityHash = profile.CapabilityHash,
                DeviceId = profile.DeviceId,
                Revision = profile.Revision,
                CreatedAt = profile.CreatedAt,
                UpdatedAt = profile.UpdatedAt,
                LastProbedAt = profile.LastProbedAt,
                LastVerifiedAt = profile.LastVerifiedAt,
                AdoptedAt = profile.AdoptedAt
            };
}

internal sealed class SetupGatewayConnector : IGatewayConnector
{
    public GatewayConnectionState ConnectionState { get; set; }
    public string? GatewayVersion { get; set; }
    public string? RequiredVersion { get; set; }
    public DateTimeOffset? LastConnectedAt { get; set; } = DateTimeOffset.UtcNow;
    public int ReconnectAttempts { get; set; }
    public string? StatusMessage { get; set; }
    public string? DeviceId { get; set; }
    public bool DeviceTokenConfigured { get; set; }
    public bool PairingRequired { get; set; }
    public string? PairingRequestId { get; set; }
    public int? ProtocolVersion { get; set; }
    public IReadOnlySet<string> AdvertisedMethods { get; set; } =
        new HashSet<string>(StringComparer.Ordinal);
    public IReadOnlySet<string> AdvertisedEvents { get; set; } =
        new HashSet<string>(StringComparer.Ordinal);
    public IReadOnlySet<string> GrantedScopes { get; set; } =
        new HashSet<string>(StringComparer.Ordinal);
    public string? ActiveEndpoint { get; set; } = "ws://127.0.0.1:18789/";
    public string? ActiveTlsFingerprint { get; set; }
    public DateTimeOffset? LastEventAt { get; set; }
    public Func<string, JsonNode?, JsonNode?>? Handler { get; set; }
    public List<(string Method, JsonNode? Parameters)> Invocations { get; } = [];
    public List<string> ConfiguredEndpoints { get; } = [];
    public bool BootstrapTokenSupplied { get; private set; }
    public List<HashSet<string>> RequestedScopeSets { get; } = [];
    public bool DisconnectRequested { get; private set; }

    public Task RequestOperatorScopesAsync(
        IReadOnlyCollection<string> scopes,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        RequestedScopeSets.Add(scopes.ToHashSet(StringComparer.Ordinal));
        return Task.CompletedTask;
    }

    public Task ConfigureEndpointAsync(
        string endpoint,
        string? tlsFingerprint,
        string? bootstrapToken,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        ActiveEndpoint = endpoint;
        ActiveTlsFingerprint = tlsFingerprint;
        ConfiguredEndpoints.Add(endpoint);
        BootstrapTokenSupplied |= !string.IsNullOrWhiteSpace(bootstrapToken);
        return Task.CompletedTask;
    }

    public Task DisconnectAsync(CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        DisconnectRequested = true;
        ConnectionState = GatewayConnectionState.Disconnected;
        return Task.CompletedTask;
    }

    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)
    {
        cancellationToken.ThrowIfCancellationRequested();
        var node = parameters switch
        {
            null => null,
            JsonNode jsonNode => jsonNode.DeepClone(),
            _ => JsonSerializer.SerializeToNode(parameters)
        };
        Invocations.Add((method, node));
        return Task.FromResult(Handler?.Invoke(method, node));
    }

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