using System.Diagnostics;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Config;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.SignalR;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Planning;

// Inline fakes — test isolation beats DRY; mirrors PlanningMcpServiceTests pattern.
file sealed class E2EFakeHttpContextAccessor : IHttpContextAccessor
{
    public HttpContext? HttpContext { get; set; }
}

file sealed class E2ENullHubClients : IHubClients
{
    public IClientProxy All => E2ENullClientProxy.Instance;
    public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => E2ENullClientProxy.Instance;
    public IClientProxy Client(string connectionId) => E2ENullClientProxy.Instance;
    public IClientProxy Clients(IReadOnlyList<string> connectionIds) => E2ENullClientProxy.Instance;
    public IClientProxy Group(string groupName) => E2ENullClientProxy.Instance;
    public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => E2ENullClientProxy.Instance;
    public IClientProxy Groups(IReadOnlyList<string> groupNames) => E2ENullClientProxy.Instance;
    public IClientProxy User(string userId) => E2ENullClientProxy.Instance;
    public IClientProxy Users(IReadOnlyList<string> userIds) => E2ENullClientProxy.Instance;
}

file sealed class E2ENullClientProxy : IClientProxy
{
    public static readonly E2ENullClientProxy Instance = new();
    public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
        => Task.CompletedTask;
}

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

public sealed class PlanningEndToEndTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;
    private readonly AppSettingsRepository _settingsRepo;
    private readonly GitService _git;
    private readonly WorkerConfig _cfg;
    private readonly string _root;
    private readonly TaskStateServiceBuilder.Built _built;
    private readonly PlanningSessionManager _manager;
    private readonly DefaultHttpContext _httpContext;
    private readonly PlanningMcpContextAccessor _accessor;
    private readonly PlanningMcpService _svc;

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

        _root = Path.Combine(Path.GetTempPath(), $"cd_e2e_{Guid.NewGuid():N}");
        _git = new GitService();
        _cfg = new WorkerConfig { CentralWorktreeRoot = Path.Combine(_root, "central") };
        _settingsRepo = new AppSettingsRepository(_ctx);
        _settingsRepo.UpdateAsync(new AppSettingsEntity { WorktreeStrategy = "sibling" }).GetAwaiter().GetResult();

        _built = TaskStateServiceBuilder.Build(_db.CreateFactory());
        _manager = new PlanningSessionManager(
            _tasks, _lists, _settingsRepo, _git, _cfg, _root, _built.State, _built.Chain);

        _httpContext = new DefaultHttpContext();
        _accessor = new PlanningMcpContextAccessor(new E2EFakeHttpContextAccessor { HttpContext = _httpContext });
        var broadcaster = new HubBroadcaster(new E2EFakeHubContext());
        _svc = new PlanningMcpService(_tasks, _accessor, broadcaster, _built.State, _built.Chain);
    }

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

    [Fact]
    public async Task StartThenCreateThenFinalize_FullFlow()
    {
        var listId = Guid.NewGuid().ToString();
        var wd = Path.Combine(Path.GetTempPath(), $"cd_e2e_wd_{Guid.NewGuid():N}");
        GitRepoFixture.InitRepoWithInitialCommit(wd);
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = wd, CreatedAt = DateTime.UtcNow });

        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "Big Task",
            Status = TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(parent);

        var startCtx = await _manager.StartAsync(parent.Id, CancellationToken.None);
        Assert.True(File.Exists(Path.Combine(startCtx.WorktreePath, ".mcp.json")));

        // Wire the ambient context so _svc reads the correct parent
        _httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };

        await _svc.CreateChildTask("sub 1", null, null, null, cancellationToken: CancellationToken.None);
        await _svc.CreateChildTask("sub 2", null, null, null, cancellationToken: CancellationToken.None);

        var count = await _svc.Finalize(true, CancellationToken.None);
        Assert.Equal(2, count);

        var reload = await _tasks.GetByIdAsync(parent.Id);
        Assert.Equal(PlanningPhase.Finalized, reload!.PlanningPhase);

        var kids = await _tasks.GetChildrenAsync(parent.Id);
        // Finalize no longer auto-queues. Children stay Idle and chain-linked
        // (head unblocked, rest BlockedBy their predecessor) until the user
        // explicitly queues the plan.
        Assert.Equal(TaskStatus.Idle, kids[0].Status);
        Assert.Null(kids[0].BlockedByTaskId);
        Assert.Equal(TaskStatus.Idle, kids[1].Status);
        Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId);
    }

    // Regression: original bug was "queue never picks up planning tasks". Finalize
    // leaves children Idle; queueing the plan (the explicit user gate) must make the
    // first child claimable by the picker automatically — without a manual WakeQueue().
    [Fact]
    public async Task QueuePlanAfterFinalize_FirstChildIsClaimedByPicker_WithinDeadline()
    {
        var listId = Guid.NewGuid().ToString();
        var wd = Path.Combine(Path.GetTempPath(), $"cd_e2e_wd_{Guid.NewGuid():N}");
        GitRepoFixture.InitRepoWithInitialCommit(wd);
        await _lists.AddAsync(new ListEntity { Id = listId, Name = "L", WorkingDir = wd, CreatedAt = DateTime.UtcNow });

        var parent = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "Parent",
            Status = TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "chore",
        };
        await _tasks.AddAsync(parent);

        await _manager.StartAsync(parent.Id, CancellationToken.None);
        _httpContext.Items["PlanningContext"] = new PlanningMcpContext { ParentTaskId = parent.Id };

        await _svc.CreateChildTask("c1", null, null, null, cancellationToken: CancellationToken.None);
        await _svc.CreateChildTask("c2", null, null, null, cancellationToken: CancellationToken.None);
        await _svc.CreateChildTask("c3", null, null, null, cancellationToken: CancellationToken.None);

        var kidsBefore = await _tasks.GetChildrenAsync(parent.Id);
        var firstChildId = kidsBefore[0].Id;

        await _manager.FinalizeAsync(parent.Id, queueAgentTasks: true, CancellationToken.None);

        // Finalize leaves every child Idle — nothing is claimable yet.
        var afterFinalize = await _tasks.GetChildrenAsync(parent.Id);
        Assert.All(afterFinalize, k => Assert.Equal(TaskStatus.Idle, k.Status));

        // Queueing the plan is the gate that enqueues + auto-wakes. The picker should
        // then pick the first child immediately, without a manual WakeQueue().
        var wakesBefore = _built.WakeCount();
        await _built.Chain.QueuePlanAsync(parent.Id, CancellationToken.None);

        var picker = new QueuePicker(_db.CreateFactory());

        TaskEntity? claimed = null;
        var sw = Stopwatch.StartNew();
        while (sw.ElapsedMilliseconds < 200)
        {
            claimed = await picker.ClaimNextAsync(DateTime.UtcNow, CancellationToken.None);
            if (claimed is not null) break;
            await Task.Delay(10);
        }

        Assert.NotNull(claimed);
        Assert.Equal(firstChildId, claimed!.Id);
        Assert.Equal(TaskStatus.Running, claimed.Status);
        Assert.True(_built.WakeCount() > wakesBefore,
            "QueuePlanAsync → EnqueueAsync should auto-wake the queue.");
    }
}
