using System.ComponentModel;
using System.Reflection;
using System.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging.Abstractions;
using ModelContextProtocol.Server;
using Nexus.Api.Data;
using Nexus.Api.Integrations;
using Nexus.Api.Models;
using Nexus.Api.Repositories;
using Nexus.Api.Services;
using Xunit;

namespace Nexus.Api.Tests;

public sealed class McpToolsTests
{
    // ────────────────────────────────────────────────────────────────
    // Enum Validation
    // ────────────────────────────────────────────────────────────────

    [Fact]
    public void NexusMcpTaskState_HasExactlyFiveValidStates()
    {
        var values = Enum.GetValues<NexusMcpTaskState>();
        Assert.Equal(5, values.Length);

        var names = Enum.GetNames<NexusMcpTaskState>();
        Assert.Contains("Backlog", names);
        Assert.Contains("InProgress", names);
        Assert.Contains("Blocked", names);
        Assert.Contains("Done", names);
        Assert.Contains("Review", names);
    }

    [Theory]
    [InlineData(NexusMcpTaskState.Backlog, "Backlog")]
    [InlineData(NexusMcpTaskState.InProgress, "In progress")]
    [InlineData(NexusMcpTaskState.Blocked, "Blocked")]
    [InlineData(NexusMcpTaskState.Done, "Done")]
    [InlineData(NexusMcpTaskState.Review, "Review")]
    public void NexusMcpTaskState_MapsToCorrectStateString(NexusMcpTaskState mcpState, string expectedBridgeState)
    {
        // Verify the TaskStateHelper roundtrip works
        string stateString = mcpState switch
        {
            NexusMcpTaskState.Backlog => TaskStateHelper.ToStateString(TaskState.Backlog),
            NexusMcpTaskState.InProgress => TaskStateHelper.ToStateString(TaskState.InProgress),
            NexusMcpTaskState.Blocked => TaskStateHelper.ToStateString(TaskState.Blocked),
            NexusMcpTaskState.Done => TaskStateHelper.ToStateString(TaskState.Done),
            NexusMcpTaskState.Review => TaskStateHelper.ToStateString(TaskState.Review),
            _ => throw new ArgumentOutOfRangeException(nameof(mcpState))
        };

        Assert.Equal(expectedBridgeState, stateString);
        Assert.True(TaskStateHelper.IsValidState(stateString));
    }

    [Fact]
    public void NexusMcpTaskState_EnumValuesMatchTaskStateEnum()
    {
        // The MCP state enum must cover exactly the canonical task states
        foreach (var mcpState in Enum.GetValues<NexusMcpTaskState>())
        {
            var taskState = mcpState switch
            {
                NexusMcpTaskState.Backlog => TaskState.Backlog,
                NexusMcpTaskState.InProgress => TaskState.InProgress,
                NexusMcpTaskState.Blocked => TaskState.Blocked,
                NexusMcpTaskState.Done => TaskState.Done,
                NexusMcpTaskState.Review => TaskState.Review,
                _ => throw new ArgumentOutOfRangeException(nameof(mcpState))
            };
            Assert.True(Enum.IsDefined(taskState));
        }
    }

    // ────────────────────────────────────────────────────────────────
    // Tool Registration
    // ────────────────────────────────────────────────────────────────

    [Fact]
    public void AllRequiredTools_AreRegistered()
    {
        var toolMethods = typeof(NexusMcpTools)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .Where(m => m.GetCustomAttribute<McpServerToolAttribute>() is not null)
            .Select(m => m.GetCustomAttribute<McpServerToolAttribute>()!.Name!)
            .OrderBy(n => n)
            .ToList();

        var expected = new[]
        {
            "nexus_agent_overview",
            "nexus_append_activity",
            "nexus_create_child_task",
            "nexus_create_task",
            "nexus_get_activity",
            "nexus_get_agent_proposal",
            "nexus_get_board",
            "nexus_get_children",
            "nexus_get_task",
            "nexus_handoff",
            "nexus_propose_agent",
            "nexus_update_status"
        }.OrderBy(n => n).ToList();

        Assert.Equal(expected, toolMethods);
        Assert.Equal(12, toolMethods.Count);
    }

    [Fact]
    public void NexusMcpTools_HasMcpServerToolTypeAttribute()
    {
        var attr = typeof(NexusMcpTools).GetCustomAttribute<McpServerToolTypeAttribute>();
        Assert.NotNull(attr);
    }

    [Fact]
    public void AllTools_HaveDescriptionAttribute()
    {
        var methods = typeof(NexusMcpTools)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .Where(m => m.GetCustomAttribute<McpServerToolAttribute>() is not null);

        foreach (var method in methods)
        {
            var desc = method.GetCustomAttribute<DescriptionAttribute>();
            Assert.NotNull(desc);
            Assert.False(string.IsNullOrWhiteSpace(desc!.Description),
                $"Tool {method.Name} is missing a description.");
        }
    }

    [Fact]
    public void NexusMcpTools_AllMethodsAreAsync()
    {
        var methods = typeof(NexusMcpTools)
            .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
            .Where(m => m.GetCustomAttribute<McpServerToolAttribute>() is not null);

        foreach (var method in methods)
        {
            Assert.True(
                method.ReturnType.Name.StartsWith("Task") ||
                method.ReturnType.Name.StartsWith("ValueTask"),
                $"Tool {method.Name} does not return Task.");
        }
    }

    // ────────────────────────────────────────────────────────────────
    // Tool Behavior via Fixture (integration-style)
    // ────────────────────────────────────────────────────────────────

    [Fact]
    public async Task CreateTask_ReturnsSuccess_ForValidInput()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var result = await fixture.Tools.CreateTask("MCP Test Task", "MCP detail", "High", "iris");

        Assert.NotNull(result);
        Assert.True(result.Ok);
        Assert.Equal("nexus_create_task", result.Command);
        Assert.NotNull(result.Data);
        Assert.Null(result.Error);
        Assert.Equal("MCP Test Task", result.Data!.Title);
    }

    [Fact]
    public async Task CreateChildTask_ReturnsSuccess_ForValidParent()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var parent = await fixture.Tools.CreateTask("Parent Task", "Parent detail");
        Assert.True(parent.Ok && parent.Data is not null);

        var child = await fixture.Tools.CreateChildTask(
            parent.Data!.Id, "Child Task", "Child detail", "Normal", "programmer");

        Assert.True(child.Ok);
        Assert.Equal("nexus_create_child_task", child.Command);
        Assert.NotNull(child.Data);
        Assert.Equal("Child Task", child.Data!.Title);
        Assert.Equal(parent.Data.Id, child.Data.ParentTaskId);
    }

    [Fact]
    public async Task CreateChildTask_ReturnsError_ForMissingParent()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var result = await fixture.Tools.CreateChildTask(
            Guid.NewGuid(), "Orphan Child");

        Assert.False(result.Ok);
        Assert.Contains("not found", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task GetBoard_ReturnsGroupedTasks()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        // Create test tasks
        await fixture.Tools.CreateTask("Board Task 1", assignedTo: "iris");
        await fixture.Tools.CreateTask("Board Task 2", assignedTo: "programmer");

        var board = await fixture.Tools.GetBoard();

        Assert.NotNull(board);
        Assert.NotNull(board.Offen);
        Assert.NotNull(board.InProgress);
        Assert.NotNull(board.Review);
        Assert.NotNull(board.Blocked);
        Assert.NotNull(board.Done);
        Assert.True(board.Offen.Count >= 2);
    }

    [Fact]
    public async Task GetTask_ReturnsTask_WhenFound()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var created = await fixture.Tools.CreateTask("GetTask Test");
        Assert.True(created.Ok && created.Data is not null);

        var fetched = await fixture.Tools.GetTask(created.Data.Id);

        Assert.True(fetched.Ok);
        Assert.Equal("GetTask Test", fetched.Data!.Title);
    }

    [Fact]
    public async Task GetTask_ReturnsError_WhenNotFound()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var result = await fixture.Tools.GetTask(Guid.NewGuid());

        Assert.False(result.Ok);
        Assert.Contains("not found", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Fact]
    public async Task GetChildren_ReturnsChildTasks()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var parent = await fixture.Tools.CreateTask("Parent for Children");
        Assert.True(parent.Ok && parent.Data is not null);

        await fixture.Tools.CreateChildTask(parent.Data.Id, "Child 1");
        await fixture.Tools.CreateChildTask(parent.Data.Id, "Child 2");

        var children = await fixture.Tools.GetChildren(parent.Data.Id);

        Assert.NotNull(children);
        Assert.Equal(2, children.Count);
    }

    [Fact]
    public async Task UpdateStatus_AdvancesState()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var task = await fixture.Tools.CreateTask("Status Test");
        Assert.True(task.Ok && task.Data is not null);
        Assert.Equal("Backlog", task.Data.State);

        var inProgress = await fixture.Tools.UpdateStatus(task.Data.Id, NexusMcpTaskState.InProgress);
        Assert.True(inProgress.Ok);
        Assert.Equal("In progress", inProgress.Data!.State);

        var done = await fixture.Tools.UpdateStatus(task.Data.Id, NexusMcpTaskState.Done);
        Assert.True(done.Ok);
        Assert.Equal("Done", done.Data!.State);
    }

    [Fact]
    public async Task UpdateStatus_Unauthorized_ForSubAgent()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("programmer");

        var task = await fixture.Tools.CreateTask("SubAgent Status Test");
        Assert.True(task.Ok && task.Data is not null);

        // The programmer creates the task fine, but cannot change status
        var result = await fixture.Tools.UpdateStatus(task.Data.Id, NexusMcpTaskState.InProgress);

        Assert.False(result.Ok);
        Assert.Contains("not authorized", result.Error, StringComparison.OrdinalIgnoreCase);
    }

    [Theory]
    [InlineData(NexusMcpTaskState.Backlog)]
    [InlineData(NexusMcpTaskState.InProgress)]
    [InlineData(NexusMcpTaskState.Review)]
    [InlineData(NexusMcpTaskState.Blocked)]
    [InlineData(NexusMcpTaskState.Done)]
    public async Task UpdateStatus_AcceptsAllValidStates(NexusMcpTaskState state)
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var task = await fixture.Tools.CreateTask($"StateTest-{state}");
        Assert.True(task.Ok && task.Data is not null);

        var result = await fixture.Tools.UpdateStatus(task.Data.Id, state);
        Assert.True(result.Ok);
    }

    [Fact]
    public async Task AppendActivity_WritesActivityEntry()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var task = await fixture.Tools.CreateTask("Activity Test");
        Assert.True(task.Ok && task.Data is not null);

        var result = await fixture.Tools.AppendActivity(task.Data.Id, "Test checkpoint", "checkpoint");
        Assert.True(result.Ok);
        Assert.NotNull(result.Data);
        Assert.Equal("Test checkpoint", result.Data!.Message);

        var activities = await fixture.Tools.GetActivity(task.Data.Id);
        Assert.NotEmpty(activities);
    }

    [Fact]
    public async Task Handoff_UpdatesExpectedFrom()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var task = await fixture.Tools.CreateTask("Handoff Test");
        Assert.True(task.Ok && task.Data is not null);

        var result = await fixture.Tools.Handoff(task.Data.Id, "programmer", "Please implement");
        Assert.True(result.Ok);
        Assert.Equal("programmer", result.Data!.ExpectedFrom);
    }

    [Fact]
    public async Task GetAgentOverview_ReturnsGroupedWorkflow()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("iris");

        var overview = await fixture.Tools.GetAgentOverview(staleHours: 2);

        Assert.NotNull(overview);
        Assert.NotNull(overview.WaitingForBao);
        Assert.NotNull(overview.WaitingForIris);
        Assert.NotNull(overview.WaitingForOthers);
        Assert.NotNull(overview.StaleTasks);
    }

    // ────────────────────────────────────────────────────────────────
    // Auth Resolution
    // ────────────────────────────────────────────────────────────────

    [Fact]
    public async Task ResolveCaller_AcceptsValidXAgentId()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgent("programmer");

        // Simply verify a tool call succeeds with a valid agent header
        var result = await fixture.Tools.CreateTask("Auth Test via header");
        Assert.True(result.Ok);
    }

    [Fact]
    public async Task ResolveCaller_AcceptsJwtClaim()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerUser("iris", "member");

        var result = await fixture.Tools.CreateTask("Auth Test via JWT");
        Assert.True(result.Ok);
    }

    [Fact]
    public async Task ResolveCaller_AcceptsOwnerRole()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerUser("owner", "owner");

        var result = await fixture.Tools.CreateTask("Auth Test via owner JWT");
        Assert.True(result.Ok);
    }

    [Fact]
    public async Task ResolveCaller_AcceptsServiceKey()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerServiceKey("test-service-key");

        var result = await fixture.Tools.CreateTask("Auth Test via service key");
        Assert.True(result.Ok);
    }

    [Fact]
    public async Task ResolveCaller_RejectsUnknownAgentId()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        fixture.SetCallerAgentWithoutAuthentication("hacker");

        await Assert.ThrowsAsync<UnauthorizedAccessException>(
            () => fixture.Tools.CreateTask("Should fail"));
    }

    [Fact]
    public async Task ResolveCaller_RejectsMissingAuth()
    {
        await using var fixture = await McpToolsFixture.CreateAsync();
        // No auth set = should reject

        await Assert.ThrowsAsync<UnauthorizedAccessException>(
            () => fixture.Tools.CreateTask("Should fail"));
    }
}

// ──────────────────────────────────────────────────────────────────
// Test Fixture
// ──────────────────────────────────────────────────────────────────

internal sealed class McpToolsFixture : IAsyncDisposable
{
    private readonly NexusDbContext _db;

    private McpToolsFixture(
        NexusDbContext db,
        NexusMcpTools tools,
        HttpContextAccessor httpContextAccessor,
        ITaskBridgeService taskBridgeService,
        IAgentService agentService)
    {
        _db = db;
        Tools = tools;
        HttpContextAccessor = httpContextAccessor;
        TaskBridgeService = taskBridgeService;
        AgentService = agentService;
    }

    public NexusMcpTools Tools { get; }
    public HttpContextAccessor HttpContextAccessor { get; }
    public ITaskBridgeService TaskBridgeService { get; }
    public IAgentService AgentService { get; }

    public static async Task<McpToolsFixture> CreateAsync()
    {
        var options = new DbContextOptionsBuilder<NexusDbContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

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

        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?>
            {
                ["NexusApiKey"] = "test-service-key"
            })
            .Build();

        var agentService = new AgentService(new StubOpenClawControlService());
        var liveUpdateService = new LiveUpdateService();
        var activityRepository = new ActivityRepository(db, liveUpdateService);
        var taskRepository = new TaskRepository(db);
        var notificationService = new NotificationService(db, liveUpdateService);

        var httpContextAccessor = new HttpContextAccessor();

        var staleTaskRecoveryService = new StaleTaskRecoveryService(
            taskRepository, activityRepository, liveUpdateService);

        var taskService = new TaskService(
            taskRepository,
            activityRepository,
            notificationService,
            agentService,
            httpContextAccessor,
            liveUpdateService,
            staleTaskRecoveryService);

        var taskBridgeService = new TaskBridgeService(
            taskService,
            agentService,
            activityRepository,
            notificationService,
            liveUpdateService);

        var logger = NullLogger<NexusMcpTools>.Instance;

        var tools = new NexusMcpTools(
            taskBridgeService,
            agentService,
            httpContextAccessor,
            configuration,
            logger);

        return new McpToolsFixture(db, tools, httpContextAccessor, taskBridgeService, agentService);
    }

    public async ValueTask DisposeAsync()
    {
        await _db.DisposeAsync();
    }

    public void SetCallerAgent(string agentId)
    {
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Headers["X-Agent-Id"] = agentId;
        httpContext.User = new ClaimsPrincipal(new ClaimsIdentity(
            [new Claim(ClaimTypes.Role, "Service")],
            "ApiKey"));
        HttpContextAccessor.HttpContext = httpContext;
    }

    public void SetCallerAgentWithoutAuthentication(string agentId)
    {
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Headers["X-Agent-Id"] = agentId;
        httpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
        HttpContextAccessor.HttpContext = httpContext;
    }

    public void SetCallerUser(string userId, string role)
    {
        var claims = new[]
        {
            new Claim(ClaimTypes.NameIdentifier, userId),
            new Claim(ClaimTypes.Role, role)
        };
        var httpContext = new DefaultHttpContext();
        httpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth"));
        HttpContextAccessor.HttpContext = httpContext;
    }

    public void SetCallerServiceKey(string key)
    {
        var claims = new[] { new Claim(ClaimTypes.Role, "Service") };
        var httpContext = new DefaultHttpContext();
        httpContext.Request.Headers["X-Nexus-Api-Key"] = key;
        httpContext.User = new ClaimsPrincipal(new ClaimsIdentity(claims, "ApiKey"));
        HttpContextAccessor.HttpContext = httpContext;
    }

}
