using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Nexus.Api.Data;
using Nexus.Api.Controllers;
using Nexus.Api.DTOs;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class MissionControlPhaseTests
{
    [Fact]
    public void AgentConfigSave_IsBaoOwnerOnly()
    {
        var method = typeof(AgentsController).GetMethod(nameof(AgentsController.SaveConfigFile), BindingFlags.Instance | BindingFlags.Public);

        Assert.NotNull(method);
        var authorize = method!.GetCustomAttribute<AuthorizeAttribute>();
        Assert.NotNull(authorize);
        Assert.Equal("owner", authorize!.Roles);
    }

    [Fact]
    public void TaskApprovalEndpoints_AreOwnerOnly()
    {
        var pending = typeof(TasksController).GetMethod(nameof(TasksController.GetPendingApproval), BindingFlags.Instance | BindingFlags.Public);
        var approve = typeof(TasksController).GetMethod(nameof(TasksController.Approve), BindingFlags.Instance | BindingFlags.Public);
        var reject = typeof(TasksController).GetMethod(nameof(TasksController.Reject), BindingFlags.Instance | BindingFlags.Public);

        Assert.Equal("owner", pending!.GetCustomAttribute<AuthorizeAttribute>()?.Roles);
        Assert.Equal("owner", approve!.GetCustomAttribute<AuthorizeAttribute>()?.Roles);
        Assert.Equal("owner", reject!.GetCustomAttribute<AuthorizeAttribute>()?.Roles);
    }

    [Fact]
    public void GatewayActivityRedaction_RemovesSensitiveLines()
    {
        var text = OpenClawGatewayClient.RedactSensitiveText("""
            Status: ok
            Authorization: Bearer abc.def.ghi
            Next step ready
            X-Nexus-Api-Key: secret
            """);

        Assert.Contains("Status: ok", text);
        Assert.Contains("Next step ready", text);
        Assert.DoesNotContain("Bearer abc", text);
        Assert.DoesNotContain("secret", text);
        Assert.Equal(2, text.Split("[redacted sensitive line]").Length - 1);
    }

    [Fact]
    public void AgentSummaryBuilder_ProducesStructuredNowAndTodaySummary()
    {
        var now = DateTimeOffset.UtcNow;
        var activity = new[]
        {
            new ActivityEvent
            {
                Type = "agent_task",
                Message = "programmer completed repo scan",
                CreatedAt = now.AddHours(-3)
            }
        };

        var gateway = new[]
        {
            new AgentActivityEntry("5m ago", "Authorization: Bearer hidden\nWorking on redaction", now.AddMinutes(-5)),
            new AgentActivityEntry("20m ago", "Checking task mapping", now.AddMinutes(-20))
        };

        var summary = AgentSummaryBuilder.Build(activity, gateway, now);

        Assert.Equal("gateway-session-history", summary.Now.Source);
        Assert.Equal(now.AddMinutes(-5), summary.Now.Timestamp);
        Assert.DoesNotContain("Bearer hidden", summary.Now.Text);
        Assert.Contains("Working on redaction", summary.Now.Text);
        Assert.Equal("derived-mixed", summary.Today.Source);
        Assert.Equal(now.AddMinutes(-5), summary.Today.Timestamp);
        Assert.Contains("Working on redaction", summary.Today.Text);
        Assert.Contains("Checking task mapping", summary.Today.Text);
        Assert.Contains("programmer completed repo scan", summary.Today.Text);
    }

    [Fact]
    public async Task ActivityRepository_RedactsBeforePersistenceAndPublishesAgentIds()
    {
        var options = new DbContextOptionsBuilder<NexusDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        await using var db = new NexusDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var liveUpdates = new LiveUpdateService();
        var subscription = await liveUpdates.SubscribeAsync();
        var repository = new ActivityRepository(db, liveUpdates);

        await repository.AddAsync(new ActivityEvent
        {
            Type = "agent",
            Message = "Command sent to agent programmer: Authorization: Bearer secret-token"
        });

        var stored = await repository.GetRecentAsync(1);
        Assert.Single(stored);
        Assert.DoesNotContain("secret-token", stored[0].Message);
        Assert.Contains("programmer", stored[0].Message);
        Assert.Contains("Authorization: Bearer [redacted]", stored[0].Message);

        var envelope = await subscription.Reader.ReadAsync();
        Assert.Equal("activity.created", envelope.Type);

        var payloadJson = JsonSerializer.Serialize(envelope.Payload);
        using var doc = JsonDocument.Parse(payloadJson);
        Assert.Equal("agent", doc.RootElement.GetProperty("Type").GetString());
        Assert.DoesNotContain("secret-token", doc.RootElement.GetProperty("Message").GetString());
        var agentIds = doc.RootElement.GetProperty("agentIds").EnumerateArray().Select(x => x.GetString()).ToArray();
        Assert.Contains("programmer", agentIds);
    }

    [Fact]
    public async Task ActivityRepository_GetByAgentAsync_UsesMappedAgentIds()
    {
        var options = new DbContextOptionsBuilder<NexusDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        await using var db = new NexusDbContext(options);
        await db.Database.EnsureCreatedAsync();

        var repository = new ActivityRepository(db, new LiveUpdateService());
        await repository.AddAsync(new ActivityEvent
        {
            Type = "agent",
            Message = "Command sent to agent programmer: compile module"
        });
        await repository.AddAsync(new ActivityEvent
        {
            Type = "agent",
            Message = "Command sent to agent reviewer: inspect module"
        });

        var programmerEvents = await repository.GetByAgentAsync("programmer", 10);

        Assert.Single(programmerEvents);
        Assert.True(programmerEvents[0].Message.Contains("programmer", StringComparison.OrdinalIgnoreCase));
        Assert.False(programmerEvents[0].Message.Contains("reviewer", StringComparison.OrdinalIgnoreCase));
    }

    [Fact]
    public async Task AgentConfigSave_AuditsFailureWithoutLeakingContent()
    {
        var configService = new RejectingOpenClawAgentConfigurationService();
        var activityRepo = new CapturingActivityRepository();

        var controller = new AgentsController(
            new FakeAgentService(),
            new FakeAgentRuntime(),
            activityRepo,
            configService,
            new FakeDashboardService(),
            Microsoft.Extensions.Logging.Abstractions.NullLogger<AgentsController>.Instance)
        {
            ControllerContext = new ControllerContext
            {
                HttpContext = new DefaultHttpContext
                {
                    User = new ClaimsPrincipal(new ClaimsIdentity(
                    [
                        new Claim(ClaimTypes.NameIdentifier, "bao"),
                        new Claim(ClaimTypes.Role, "owner")
                    ], "TestAuth"))
                }
            }
        };
        controller.Request.Headers["Idempotency-Key"] = "test-config-save";

        var result = await controller.SaveConfigFile(
            "programmer",
            "TOOLS.md",
            new SaveConfigRequest("secret\0payload", "expected-hash"),
            CancellationToken.None);

        var statusResult = Assert.IsAssignableFrom<IStatusCodeHttpResult>(result);
        Assert.Equal(StatusCodes.Status400BadRequest, statusResult.StatusCode);
        var audit = Assert.Single(activityRepo.Added);
        Assert.Equal("config_audit", audit.Type);
        Assert.Contains("validation=failed", audit.Message);
        Assert.DoesNotContain("secret", audit.Message, StringComparison.OrdinalIgnoreCase);
        Assert.DoesNotContain("payload", audit.Message, StringComparison.OrdinalIgnoreCase);
    }

}

file sealed class RejectingOpenClawAgentConfigurationService
    : IOpenClawAgentConfigurationService
{
    public Task<OpenClawAgentFileCollectionDto> GetAgentFilesAsync(
        string agentId,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawAgentFileDto> GetAgentFileAsync(
        string agentId,
        string fileName,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawAgentFileWriteDto> SetAgentFileAsync(
        string agentId,
        string fileName,
        UpdateOpenClawAgentFileRequest request,
        OpenClawInvocationContext invocationContext,
        CancellationToken cancellationToken = default)
        => throw new OpenClawAgentConfigurationValidationException(
            "content",
            "Content contains null bytes.");

    public Task<OpenClawWorkspaceCollectionDto> GetWorkspaceAsync(
        string agentId,
        string? path,
        int offset,
        int limit,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawWorkspaceFileDto> GetWorkspaceFileAsync(
        string agentId,
        string path,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawConfigSchemaLookupDto> GetConfigSchemaAsync(
        string path,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawConfigSnapshotDto> GetConfigAsync(
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();

    public Task<OpenClawConfigPatchDto> PatchConfigAsync(
        PatchOpenClawConfigRequest request,
        OpenClawInvocationContext invocationContext,
        CancellationToken cancellationToken = default)
        => throw new NotSupportedException();
}

file sealed class CapturingActivityRepository : IActivityRepository
{
    public List<ActivityEvent> Added { get; } = [];

    public Task<List<ActivityEvent>> GetRecentAsync(int take, CancellationToken ct = default) => Task.FromResult(new List<ActivityEvent>());
    public Task<List<ActivityEvent>> GetRecentForTasksAsync(IEnumerable<Guid> taskIds, CancellationToken ct = default) => Task.FromResult(new List<ActivityEvent>());
    public Task<(List<ActivityEvent> Items, int TotalCount)> GetPagedAsync(string? type, string? sort, int page, int pageSize, CancellationToken ct = default)
        => Task.FromResult((new List<ActivityEvent>(), 0));
    public Task<List<ActivityEvent>> GetByAgentAsync(string agentId, int take, CancellationToken ct = default) => Task.FromResult(new List<ActivityEvent>());
    public Task<ActivityEvent> AddAsync(ActivityEvent activity, CancellationToken ct = default)
    {
        Added.Add(activity);
        return Task.FromResult(activity);
    }
}

file sealed class FakeAgentService : IAgentService
{
    public Task<IReadOnlyCollection<AgentInfo>> GetAgentsAsync(CancellationToken cancellationToken)
        => Task.FromResult<IReadOnlyCollection<AgentInfo>>([]);

    public Task<AgentDetail?> GetAgentAsync(string id, CancellationToken cancellationToken)
        => Task.FromResult<AgentDetail?>(null);

    public Task<IReadOnlySet<string>> GetAllowedAgentIdsAsync(CancellationToken cancellationToken)
        => Task.FromResult<IReadOnlySet<string>>(new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "iris", "bao", "programmer" });
}

file sealed class FakeAgentRuntime :
    Nexus.Api.Integrations.IAgentRuntime,
    IOpenClawChatService
{
    public string Name => "fake";

    public Task<Nexus.Api.Integrations.AgentRuntimeStatus> GetStatusAsync(CancellationToken cancellationToken)
        => Task.FromResult(new Nexus.Api.Integrations.AgentRuntimeStatus("fake", OperationalStatus.Online, TimeSpan.Zero, null));

    public Task<Nexus.Api.Integrations.AgentChatResult> ChatAsync(string message, string conversationId, string agentId, CancellationToken cancellationToken)
        => Task.FromResult(new Nexus.Api.Integrations.AgentChatResult("fake", agentId, conversationId, "ok"));

    public Task<Nexus.Api.Integrations.AgentChatResult> SendAsync(
        string message,
        string conversationId,
        string agentId,
        OpenClawInvocationMetadata invocation,
        CancellationToken cancellationToken = default)
        => ChatAsync(message, conversationId, agentId, cancellationToken);
}

file sealed class FakeDashboardService : IDashboardService
{
    public Task<DashboardStatus> GetStatusAsync() => Task.FromResult(new DashboardStatus(true, "online", 1, 0));
    public Task<List<DashboardAgentInfo>> GetAgentsAsync() => Task.FromResult(new List<DashboardAgentInfo>());
    public Task<List<FeedEntry>> GetOperationsAsync(int limit, string? agentFilter) => Task.FromResult(new List<FeedEntry>());
    public Task<ChatResponse> SendChatAsync(string agentId, string message) => Task.FromResult(new ChatResponse(true, "", null));
    public Task<List<MessageEntry>> GetMessagesAsync(string? sessionKey, int limit, int offset) => Task.FromResult(new List<MessageEntry>());
    public Task<List<QueueItem>> GetQueueAsync(CancellationToken ct) => Task.FromResult(new List<QueueItem>());
    public Task<GatewayRuntimeInfo> GetGatewayInfoAsync(CancellationToken ct) => Task.FromResult(new GatewayRuntimeInfo(true, "http://gateway", "test", "test", true, true, "matched", DateTimeOffset.UtcNow, "ok"));
    public Task<QueueDeleteResult> DeleteQueueItemAsync(string id, string? source, CancellationToken ct) => Task.FromResult(new QueueDeleteResult(QueueDeleteOutcome.Ignored));
    public Task<QueuePriorityResult> CycleQueuePriorityAsync(string id, CancellationToken ct) => Task.FromResult(new QueuePriorityResult(QueuePriorityOutcome.Ignored));
    public Task<AgentModelInfo?> GetAgentModelAsync(string agentId) => Task.FromResult<AgentModelInfo?>(null);
    public Task<bool> SetAgentModelAsync(string agentId, string model) => Task.FromResult(false);
    public Task<List<AgentActivityEntry>> GetAgentActivityAsync(string agentId, int limit) => Task.FromResult(new List<AgentActivityEntry>());
    public Task<List<ModelOption>> GetAvailableModelsAsync(CancellationToken ct)
        => Task.FromResult(new List<ModelOption>());
}
