using System.Reflection;
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Nexus.Api.Controllers;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class OpenClawEventProjectionTests
{
    [Fact]
    public void Project_OrdersEvents_RedactsSecrets_AndReportsSequenceGap()
    {
        var now = DateTimeOffset.UtcNow;
        var connector = new EventConnector
        {
            Events =
            [
                new GatewayEventEnvelope(
                    "chat",
                    new JsonObject
                    {
                        ["runId"] = "run-1",
                        ["state"] = "delta",
                        ["seq"] = 2,
                        ["apiToken"] = "do-not-leak",
                        ["inputTokens"] = 42
                    },
                    12,
                    4,
                    now.AddMilliseconds(20)),
                new GatewayEventEnvelope(
                    "sessions.changed",
                    new JsonObject { ["sessionKey"] = "agent:iris:main" },
                    10,
                    3,
                    now)
            ]
        };
        var service = new OpenClawEventProjectionService(connector);

        var batch = service.Project(null);

        Assert.Equal(2, batch.Events.Count);
        Assert.Equal("sessions.changed", batch.Events[0].EventName);
        Assert.Equal("chat", batch.Events[1].EventName);
        Assert.True(batch.Events[1].SequenceGapDetected);
        Assert.Equal(11, batch.Events[1].MissingSequenceFrom);
        Assert.Equal(11, batch.Events[1].MissingSequenceTo);
        Assert.Equal("[redacted]", batch.Events[1].Payload?["apiToken"]?.GetValue<string>());
        Assert.Equal(42, batch.Events[1].Payload?["inputTokens"]?.GetValue<int>());
        Assert.StartsWith("gw-", batch.Events[0].Id);
        Assert.Equal(batch.Events[^1].Id, batch.Cursor);
    }

    [Fact]
    public void Project_ReplaysAfterKnownCursor_AndSignalsExpiredCursor()
    {
        var now = DateTimeOffset.UtcNow;
        var connector = new EventConnector
        {
            Events =
            [
                Event("chat", 3, now.AddSeconds(2)),
                Event("session.tool", 2, now.AddSeconds(1)),
                Event("sessions.changed", 1, now)
            ]
        };
        var service = new OpenClawEventProjectionService(connector);
        var initial = service.Project(null);

        var replay = service.Project(initial.Events[0].Id);
        var expired = service.Project("gw-expired");

        Assert.Equal(2, replay.Events.Count);
        Assert.False(replay.ReplayBoundaryMissed);
        Assert.True(expired.ReplayBoundaryMissed);
        Assert.Equal(3, expired.Events.Count);
        Assert.Equal(initial.Events[^1].Id, expired.Cursor);
    }

    [Fact]
    public void Project_SignalsExpiredCursorAfterBackendRestartWithEmptyBuffer()
    {
        var service = new OpenClawEventProjectionService(new EventConnector());

        var batch = service.Project("gw-from-previous-process");

        Assert.True(batch.ReplayBoundaryMissed);
        Assert.Empty(batch.Events);
        Assert.Equal("origin", batch.Cursor);
    }

    [Fact]
    public void Project_ReportsOuterSequenceResetWithoutCallingItAMissingRange()
    {
        var now = DateTimeOffset.UtcNow;
        var connector = new EventConnector
        {
            Events =
            [
                Event("chat", 1, now.AddSeconds(1)),
                Event("chat", 80, now)
            ]
        };
        var service = new OpenClawEventProjectionService(connector);

        var batch = service.Project(null);

        Assert.True(batch.Events[1].SequenceResetDetected);
        Assert.False(batch.Events[1].SequenceGapDetected);
        Assert.Equal(80, batch.Events[1].PreviousSequence);
        Assert.Equal(1, batch.Events[1].Sequence);
    }

    [Theory]
    [InlineData("sessions.changed", "session")]
    [InlineData("session.tool", "tool")]
    [InlineData("exec.approval.requested", "approval")]
    [InlineData("artifact.created", "artifact")]
    [InlineData("chat", "run")]
    public void Project_ClassifiesOperationalEventFamilies(string eventName, string category)
    {
        var connector = new EventConnector
        {
            Events = [Event(eventName, 1, DateTimeOffset.UtcNow)]
        };

        var item = Assert.Single(new OpenClawEventProjectionService(connector).Project(null).Events);

        Assert.Equal(category, item.Category);
        Assert.Equal($"openclaw.{category}", item.Type);
    }

    [Fact]
    public async Task Controller_UsesLastEventId_AndCanReturnFiniteReplay()
    {
        var connector = new EventConnector
        {
            Events = [Event("chat", 7, DateTimeOffset.UtcNow)]
        };
        var projector = new OpenClawEventProjectionService(connector);
        var controller = new OpenClawEventsController(projector);
        var context = new DefaultHttpContext();
        context.Request.Headers["Last-Event-ID"] = "gw-expired";
        context.Response.Body = new MemoryStream();
        controller.ControllerContext = new ControllerContext { HttpContext = context };

        await controller.Stream(follow: false);

        context.Response.Body.Position = 0;
        using var reader = new StreamReader(context.Response.Body, Encoding.UTF8);
        var body = await reader.ReadToEndAsync();
        Assert.Equal("text/event-stream", context.Response.ContentType);
        Assert.Contains("event: openclaw.connection", body, StringComparison.Ordinal);
        Assert.Contains("event: openclaw.gap", body, StringComparison.Ordinal);
        Assert.Contains("event: openclaw.run", body, StringComparison.Ordinal);
        Assert.Contains("event: openclaw.heartbeat", body, StringComparison.Ordinal);
        Assert.Contains("last-event-id-outside-buffer", body, StringComparison.Ordinal);
    }

    [Fact]
    public void Controller_IsAuthenticated()
    {
        var authorize = typeof(OpenClawEventsController)
            .GetCustomAttributes<AuthorizeAttribute>()
            .SingleOrDefault();

        Assert.NotNull(authorize);
    }

    private static GatewayEventEnvelope Event(
        string name,
        long sequence,
        DateTimeOffset receivedAt)
        => new(
            name,
            new JsonObject
            {
                ["runId"] = "run-1",
                ["state"] = "delta",
                ["seq"] = sequence
            },
            sequence,
            sequence,
            receivedAt);

    private sealed class EventConnector : IGatewayConnector
    {
        public IReadOnlyList<GatewayEventEnvelope> Events { get; init; } = [];
        public GatewayConnectionState ConnectionState => GatewayConnectionState.Connected;
        public string? GatewayVersion => "2026.7.0";
        public string? RequiredVersion => "2026.7.0";
        public DateTimeOffset? LastConnectedAt => DateTimeOffset.UtcNow;
        public int ReconnectAttempts => 0;
        public string? StatusMessage => "Connected";
        public string? DeviceId => "nexus-test";
        public bool DeviceTokenConfigured => true;
        public bool PairingRequired => false;
        public string? PairingRequestId => null;
        public int? ProtocolVersion => 4;
        public IReadOnlySet<string> AdvertisedMethods { get; } = new HashSet<string>();
        public IReadOnlySet<string> AdvertisedEvents { get; } = new HashSet<string>();
        public IReadOnlySet<string> GrantedScopes { get; } = new HashSet<string>();
        public DateTimeOffset? LastEventAt => Events.FirstOrDefault()?.ReceivedAt;

        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)
            => Events.Take(limit).ToList();
    }
}
