using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Planning;

// Minimal fakes — avoids Moq dependency.
file sealed class FakeHttpContextAccessor : IHttpContextAccessor
{
    public HttpContext? HttpContext { get; set; }
}

file sealed class RecordingHubClients : IHubClients
{
    public RecordingClientProxy Proxy { get; } = new();
    public IClientProxy All => Proxy;
    public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => Proxy;
    public IClientProxy Client(string connectionId) => Proxy;
    public IClientProxy Clients(IReadOnlyList<string> connectionIds) => Proxy;
    public IClientProxy Group(string groupName) => Proxy;
    public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => Proxy;
    public IClientProxy Groups(IReadOnlyList<string> groupNames) => Proxy;
    public IClientProxy User(string userId) => Proxy;
    public IClientProxy Users(IReadOnlyList<string> userIds) => Proxy;
}

file sealed class RecordingClientProxy : IClientProxy
{
    public List<(string Method, object?[] Args)> Calls { get; } = new();
    public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
    {
        Calls.Add((method, args));
        return Task.CompletedTask;
    }
}

file sealed class FakeHubContext : IHubContext<WorkerHub>
{
    public RecordingHubClients RecordingClients { get; } = new();
    public IHubClients Clients => RecordingClients;
    public IGroupManager Groups => throw new NotImplementedException();
}

public sealed class PlanningMcpServiceTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;

    public PlanningMcpServiceTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
    }

    public void Dispose() { _ctx.Dispose(); _db.Dispose(); }

    private List<(string Method, object?[] Args)> _hubCalls = new();
    private TaskStateServiceBuilder.Built? _built;

    private PlanningMcpService BuildSut(string parentTaskId)
    {
        var httpContext = new DefaultHttpContext();
        httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parentTaskId };
        var accessor = new PlanningMcpContextAccessor(new FakeHttpContextAccessor { HttpContext = httpContext });
        var hub = new FakeHubContext();
        _hubCalls = hub.RecordingClients.Proxy.Calls;
        var broadcaster = new HubBroadcaster(hub);
        _built = TaskStateServiceBuilder.Build(_db.CreateFactory());
        return new PlanningMcpService(_tasks, accessor, broadcaster, _built.State, _built.Chain);
    }

    private IReadOnlyList<string> TaskUpdatedIds() =>
        _hubCalls
            .Where(c => c.Method == "TaskUpdated")
            .Select(c => (string)c.Args[0]!)
            .ToList();

    private async Task<TaskEntity> SeedPlanningParentAsync()
    {
        var listId = Guid.NewGuid().ToString();
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "p",
            Status = TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(parent);
        await _tasks.SetPlanningStartedAsync(parent.Id, "tok");
        return (await _tasks.GetByIdAsync(parent.Id))!;
    }

    [Fact]
    public async Task CreateChildTask_CreatesDraft()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        var result = await sut.CreateChildTask("My child", "desc", null, model: null, cancellationToken: CancellationToken.None);

        Assert.Equal("Idle", result.Status);
        var child = await _tasks.GetByIdAsync(result.TaskId);
        Assert.Equal("My child", child!.Title);
        Assert.Equal(TaskStatus.Idle, child.Status);
        Assert.Null(child.Model);
    }

    [Fact]
    public async Task CreateChildTask_PersistsNormalizedModel()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        var result = await sut.CreateChildTask("c", null, null, model: "Opus", cancellationToken: CancellationToken.None);

        var child = await _tasks.GetByIdAsync(result.TaskId);
        Assert.Equal("opus", child!.Model);
    }

    [Fact]
    public async Task CreateChildTask_RejectsUnknownModel()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        await Assert.ThrowsAsync<ArgumentException>(
            () => sut.CreateChildTask("c", null, null, model: "turbo", cancellationToken: CancellationToken.None));
    }

    [Fact]
    public async Task CreateChildTask_NoMaxTurns_ChildInherits()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        var result = await sut.CreateChildTask("c", null, null, model: null, cancellationToken: CancellationToken.None);

        var child = await _tasks.GetByIdAsync(result.TaskId);
        Assert.Null(child!.MaxTurns);
    }

    [Fact]
    public async Task CreateChildTask_PersistsMaxTurns()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        var result = await sut.CreateChildTask("c", null, null, model: null, maxTurns: 200, cancellationToken: CancellationToken.None);

        var child = await _tasks.GetByIdAsync(result.TaskId);
        Assert.Equal(200, child!.MaxTurns);
    }

    [Theory]
    [InlineData(0)]
    [InlineData(-1)]
    public async Task CreateChildTask_RejectsInvalidMaxTurns(int maxTurns)
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        await Assert.ThrowsAsync<ArgumentException>(
            () => sut.CreateChildTask("c", null, null, model: null, maxTurns: maxTurns, cancellationToken: CancellationToken.None));
    }

    [Fact]
    public async Task ListChildTasks_ReturnsOnlyThisParentsChildren()
    {
        var parent = await SeedPlanningParentAsync();
        var other = await SeedPlanningParentAsync();

        await _tasks.CreateChildAsync(parent.Id, "mine", null, null);
        await _tasks.CreateChildAsync(other.Id, "theirs", null, null);

        var sut = BuildSut(parent.Id);
        var list = await sut.ListChildTasks(CancellationToken.None);
        Assert.Single(list);
        Assert.Equal("mine", list[0].Title);
    }

    [Fact]
    public async Task UpdateChildTask_NotAChild_Throws()
    {
        var parent = await SeedPlanningParentAsync();
        var other = await SeedPlanningParentAsync();
        var otherChild = await _tasks.CreateChildAsync(other.Id, "x", null, null);

        var sut = BuildSut(parent.Id);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            sut.UpdateChildTask(otherChild.Id, "new", null, null, null, CancellationToken.None));
    }

    [Fact]
    public async Task UpdateChildTask_AfterFinalize_Throws()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        // Simulate post-finalize state directly: parent.PlanningPhase=Finalized
        // is the gate the MCP service checks.
        var sut = BuildSut(parent.Id);
        var result = await _built!.State.FinalizePlanningAsync(parent.Id, CancellationToken.None);
        Assert.True(result.Ok, result.Reason);

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            sut.UpdateChildTask(c.Id, "new", null, null, null, CancellationToken.None));
    }

    [Fact]
    public async Task UpdateChildTask_SetsStatus()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        _ctx.ChangeTracker.Clear();

        var sut = BuildSut(parent.Id);
        var result = await sut.UpdateChildTask(c.Id, null, null, null, "Queued", CancellationToken.None);

        Assert.Equal("Queued", result.Status);
        var loaded = await _tasks.GetByIdAsync(c.Id);
        Assert.Equal(TaskStatus.Queued, loaded!.Status);
    }

    [Fact]
    public async Task UpdateChildTask_DisallowedStatus_Throws()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        _ctx.ChangeTracker.Clear();

        var sut = BuildSut(parent.Id);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            sut.UpdateChildTask(c.Id, null, null, null, "Running", CancellationToken.None));
    }

    [Fact]
    public async Task UpdateChildTask_UnknownStatus_Throws()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        _ctx.ChangeTracker.Clear();

        var sut = BuildSut(parent.Id);
        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            sut.UpdateChildTask(c.Id, null, null, null, "NotARealStatus", CancellationToken.None));
    }

    [Fact]
    public async Task DeleteChildTask_RemovesDraft()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);

        var sut = BuildSut(parent.Id);
        await sut.DeleteChildTask(c.Id, CancellationToken.None);

        Assert.Null(await _tasks.GetByIdAsync(c.Id));
    }

    [Fact]
    public async Task UpdatePlanningTask_SetsTitleAndDescription()
    {
        var parent = await SeedPlanningParentAsync();

        var sut = BuildSut(parent.Id);
        await sut.UpdatePlanningTask("new title", "new desc", CancellationToken.None);

        var loaded = await _tasks.GetByIdAsync(parent.Id);
        Assert.Equal("new title", loaded!.Title);
        Assert.Equal("new desc", loaded.Description);
    }

    [Fact]
    public async Task Finalize_PromotesDraftsAndInvalidatesToken()
    {
        var parent = await SeedPlanningParentAsync();
        await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
        await _tasks.CreateChildAsync(parent.Id, "c2", null, null);

        var sut = BuildSut(parent.Id);
        var count = await sut.Finalize(true, CancellationToken.None);

        Assert.Equal(2, count);
        var loaded = await _tasks.GetByIdAsync(parent.Id);
        Assert.Equal(PlanningPhase.Finalized, loaded!.PlanningPhase);
        Assert.Null(loaded.PlanningSessionToken);
    }

    [Fact]
    public async Task CreateChildTask_BroadcastsBothChildAndParent()
    {
        var parent = await SeedPlanningParentAsync();
        var sut = BuildSut(parent.Id);

        var result = await sut.CreateChildTask("c", null, null, model: null, cancellationToken: CancellationToken.None);

        var ids = TaskUpdatedIds();
        Assert.Contains(result.TaskId, ids);
        Assert.Contains(parent.Id, ids);
    }

    [Fact]
    public async Task UpdateChildTask_BroadcastsBothChildAndParent()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);
        _ctx.ChangeTracker.Clear();

        var sut = BuildSut(parent.Id);
        await sut.UpdateChildTask(c.Id, "new title", null, null, null, CancellationToken.None);

        var ids = TaskUpdatedIds();
        Assert.Contains(c.Id, ids);
        Assert.Contains(parent.Id, ids);
    }

    [Fact]
    public async Task DeleteChildTask_BroadcastsBothChildAndParent()
    {
        var parent = await SeedPlanningParentAsync();
        var c = await _tasks.CreateChildAsync(parent.Id, "c", null, null);

        var sut = BuildSut(parent.Id);
        await sut.DeleteChildTask(c.Id, CancellationToken.None);

        var ids = TaskUpdatedIds();
        Assert.Contains(c.Id, ids);
        Assert.Contains(parent.Id, ids);
    }

    [Fact]
    public async Task Finalize_BroadcastsEachChildAndParent()
    {
        var parent = await SeedPlanningParentAsync();
        var c1 = await _tasks.CreateChildAsync(parent.Id, "c1", null, null);
        var c2 = await _tasks.CreateChildAsync(parent.Id, "c2", null, null);

        var sut = BuildSut(parent.Id);
        await sut.Finalize(true, CancellationToken.None);

        var ids = TaskUpdatedIds();
        Assert.Contains(c1.Id, ids);
        Assert.Contains(c2.Id, ids);
        Assert.Contains(parent.Id, ids);
    }
}
