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.Agents;
using ClaudeDo.Worker.Lifecycle;
using ClaudeDo.Worker.Planning;
using ClaudeDo.Worker.Queue;
using ClaudeDo.Worker.Worktrees;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Hub;

public sealed class PlanningHubTests : IDisposable
{
    private readonly DbFixture _db = new();
    private readonly ClaudeDoDbContext _ctx;
    private readonly TaskRepository _tasks;
    private readonly ListRepository _lists;
    private readonly string _rootDir;
    private readonly PlanningSessionManager _planning;
    private readonly FakeTerminalLauncher _launcher;
    private readonly RecordingClientProxy _proxy;

    public PlanningHubTests()
    {
        _ctx = _db.CreateContext();
        _tasks = new TaskRepository(_ctx);
        _lists = new ListRepository(_ctx);
        _rootDir = Path.Combine(Path.GetTempPath(), $"cd_hub_planning_{Guid.NewGuid():N}");
        var git = new GitService();
        var cfg = new WorkerConfig { CentralWorktreeRoot = Path.Combine(_rootDir, "central") };
        var settingsRepo = new AppSettingsRepository(_ctx);
        settingsRepo.UpdateAsync(new AppSettingsEntity { WorktreeStrategy = "sibling" }).GetAwaiter().GetResult();
        var built = TaskStateServiceBuilder.Build(_db.CreateFactory());
        _planning = new PlanningSessionManager(
            _tasks, _lists, settingsRepo, git, cfg, _rootDir, built.State, built.Chain);
        _launcher = new FakeTerminalLauncher();
        _proxy = new RecordingClientProxy();
    }

    public void Dispose()
    {
        _ctx.Dispose();
        _db.Dispose();
        try { Directory.Delete(_rootDir, recursive: true); } catch { }
    }

    private WorkerHub CreateHub()
    {
        var hub = new WorkerHub(
            null!, null!, null!, null!, null!, _db.CreateFactory(), null!, null!, null!,
            _planning, _launcher, null!, null!, null!, null!, null!, null!, null!, null!,
            null!, new ClaudeDo.Worker.Online.OnlineInboxConfig(), new ClaudeDo.Worker.OnlineRefreshTokenStore(new ClaudeDo.Worker.DpapiTokenStore("unused.token")),
            new ClaudeDo.Worker.Runner.PendingQuestionRegistry(), null!);
        hub.Clients = new FakeHubCallerClients(_proxy);
        hub.Context = new FakeHubCallerContext();
        return hub;
    }

    private async Task<(string listId, string taskId)> SeedAsync()
    {
        var listId = Guid.NewGuid().ToString();
        var wd = Path.Combine(Path.GetTempPath(), $"cd_wd_{Guid.NewGuid():N}");
        GitRepoFixture.InitRepoWithInitialCommit(wd);
        await _lists.AddAsync(new ListEntity
        {
            Id = listId, Name = "L", WorkingDir = wd, CreatedAt = DateTime.UtcNow,
        });
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = listId,
            Title = "Do something",
            Status = TaskStatus.Idle,
            CreatedAt = DateTime.UtcNow,
            CommitType = "feat",
        };
        await _tasks.AddAsync(task);
        return (listId, task.Id);
    }

    [Fact]
    public async Task StartPlanningSessionAsync_ChangesStatusToPlanning_AndInvokesLauncher()
    {
        var (_, taskId) = await SeedAsync();
        var hub = CreateHub();

        var ctx = await hub.StartPlanningSessionAsync(taskId);

        Assert.Equal(taskId, ctx.ParentTaskId);
        Assert.Equal(1, _launcher.LaunchStartCalls);
        Assert.Equal(0, _launcher.LaunchResumeCalls);

        var loaded = await _tasks.GetByIdAsync(taskId);
        Assert.Equal(TaskStatus.Idle, loaded!.Status);
        Assert.Equal(PlanningPhase.Active, loaded.PlanningPhase);

        Assert.Contains(_proxy.Sent, m => m.method == "TaskUpdated");
    }

    [Fact]
    public async Task StartPlanningSessionAsync_LauncherFails_Discards()
    {
        var (_, taskId) = await SeedAsync();
        _launcher.ShouldThrow = true;
        var hub = CreateHub();

        await Assert.ThrowsAsync<TerminalLaunchException>(() =>
            hub.StartPlanningSessionAsync(taskId));

        var loaded = await _tasks.GetByIdAsync(taskId);
        Assert.Equal(TaskStatus.Idle, loaded!.Status);
        Assert.Equal(PlanningPhase.None, loaded.PlanningPhase);

        var sessionDir = Path.Combine(_rootDir, taskId);
        Assert.False(Directory.Exists(sessionDir));
    }

    [Fact]
    public async Task DiscardPlanningSessionAsync_ResetsTask_AndBroadcasts()
    {
        var (_, taskId) = await SeedAsync();
        // Put task into Planning state first
        await _planning.StartAsync(taskId, CancellationToken.None);
        _proxy.Sent.Clear();

        var hub = CreateHub();
        await hub.DiscardPlanningSessionAsync(taskId);

        var loaded = await _tasks.GetByIdAsync(taskId);
        Assert.Equal(TaskStatus.Idle, loaded!.Status);
        Assert.Equal(PlanningPhase.None, loaded.PlanningPhase);
        Assert.Contains(_proxy.Sent, m => m.method == "TaskUpdated");
    }

    [Fact]
    public async Task FinalizePlanningSessionAsync_PromotesDraftsAndBroadcasts()
    {
        var (_, taskId) = await SeedAsync();
        await _planning.StartAsync(taskId, CancellationToken.None);
        await _tasks.CreateChildAsync(taskId, "child 1", null, null);
        await _tasks.CreateChildAsync(taskId, "child 2", null, null);
        _proxy.Sent.Clear();

        var hub = CreateHub();
        var count = await hub.FinalizePlanningSessionAsync(taskId, queueAgentTasks: false);

        Assert.Equal(2, count);
        Assert.Contains(_proxy.Sent, m => m.method == "TaskUpdated");
    }

    [Fact]
    public async Task GetPendingDraftCountAsync_ReturnsCount()
    {
        var (_, taskId) = await SeedAsync();
        await _planning.StartAsync(taskId, CancellationToken.None);
        await _tasks.CreateChildAsync(taskId, "c1", null, null);
        await _tasks.CreateChildAsync(taskId, "c2", null, null);

        var hub = CreateHub();
        var count = await hub.GetPendingDraftCountAsync(taskId);

        Assert.Equal(2, count);
    }
}

// ---------------------------------------------------------------------------
// Fakes
// ---------------------------------------------------------------------------

internal sealed class FakeTerminalLauncher : ITerminalLauncher
{
    public bool ShouldThrow { get; set; }
    public int LaunchStartCalls { get; private set; }
    public int LaunchResumeCalls { get; private set; }

    public Task LaunchPlanningStartAsync(PlanningSessionStartContext ctx, CancellationToken cancellationToken)
    {
        if (ShouldThrow) throw new TerminalLaunchException("fake launch failure");
        LaunchStartCalls++;
        return Task.CompletedTask;
    }

    public Task LaunchPlanningResumeAsync(PlanningSessionResumeContext ctx, CancellationToken cancellationToken)
    {
        LaunchResumeCalls++;
        return Task.CompletedTask;
    }
}

internal sealed class RecordingClientProxy : IClientProxy
{
    public List<(string method, object?[] args)> Sent { get; } = new();

    public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
    {
        Sent.Add((method, args));
        return Task.CompletedTask;
    }
}

internal sealed class FakeHubCallerClients : IHubCallerClients
{
    private readonly IClientProxy _all;
    public FakeHubCallerClients(IClientProxy proxy) => _all = proxy;

    public IClientProxy All => _all;
    public IClientProxy Caller => _all;
    public IClientProxy Others => _all;
    public IClientProxy AllExcept(IReadOnlyList<string> excludedConnectionIds) => _all;
    public IClientProxy Client(string connectionId) => _all;
    public IClientProxy Clients(IReadOnlyList<string> connectionIds) => _all;
    public IClientProxy Group(string groupName) => _all;
    public IClientProxy GroupExcept(string groupName, IReadOnlyList<string> excludedConnectionIds) => _all;
    public IClientProxy Groups(IReadOnlyList<string> groupNames) => _all;
    public IClientProxy OthersInGroup(string groupName) => _all;
    public IClientProxy User(string userId) => _all;
    public IClientProxy Users(IReadOnlyList<string> userIds) => _all;
}

internal sealed class FakeHubCallerContext : HubCallerContext
{
    public override string ConnectionId => "test-conn";
    public override string? UserIdentifier => null;
    public override System.Security.Claims.ClaimsPrincipal? User => null;
    public override IDictionary<object, object?> Items { get; } = new Dictionary<object, object?>();
    public override Microsoft.AspNetCore.Http.Features.IFeatureCollection Features { get; } =
        new Microsoft.AspNetCore.Http.Features.FeatureCollection();
    public override CancellationToken ConnectionAborted => CancellationToken.None;
    public override void Abort() { }
}
