using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Hub;
using ClaudeDo.Worker.Runner;
using ClaudeDo.Worker.Services;
using ClaudeDo.Worker.Tests.Infrastructure;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging.Abstractions;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.Services;

public class TaskMergeServiceTests : IDisposable
{
    private readonly List<DbFixture> _dbs = new();
    private readonly List<GitRepoFixture> _repos = new();
    private readonly List<(string repoDir, string wtPath)> _wtCleanups = new();

    private DbFixture NewDb() { var d = new DbFixture(); _dbs.Add(d); return d; }
    private GitRepoFixture NewRepo() { var r = new GitRepoFixture(); _repos.Add(r); return r; }

    public void Dispose()
    {
        foreach (var (repoDir, wtPath) in _wtCleanups)
        {
            try { GitRepoFixture.RunGit(repoDir, "worktree", "remove", "--force", wtPath); } catch { }
        }
        foreach (var d in _dbs) try { d.Dispose(); } catch { }
        foreach (var r in _repos) try { r.Dispose(); } catch { }
    }

    private static (TaskMergeService svc, MergeRecordingClientProxy proxy) BuildService(DbFixture db)
    {
        var fakeHub = new MergeRecordingHubContext();
        var broadcaster = new HubBroadcaster(fakeHub);
        var svc = new TaskMergeService(
            db.CreateFactory(),
            new GitService(),
            broadcaster,
            NullLogger<TaskMergeService>.Instance);
        return (svc, fakeHub.Proxy);
    }

    private static WorktreeManager BuildWorktreeManager(DbFixture db)
    {
        return new WorktreeManager(
            new GitService(),
            db.CreateFactory(),
            new ClaudeDo.Worker.Config.WorkerConfig { WorktreeRootStrategy = "sibling" },
            NullLogger<WorktreeManager>.Instance);
    }

    private static async Task<(ListEntity list, TaskEntity task)> SeedListAndTask(
        DbFixture db, string workingDir, TaskStatus status)
    {
        var list = new ListEntity
        {
            Id = Guid.NewGuid().ToString(),
            Name = "merge-test",
            WorkingDir = workingDir,
            DefaultCommitType = "feat",
            CreatedAt = DateTime.UtcNow,
        };
        var task = new TaskEntity
        {
            Id = Guid.NewGuid().ToString(),
            ListId = list.Id,
            Title = "merge-task",
            Status = status,
            CreatedAt = DateTime.UtcNow,
        };
        using var ctx = db.CreateContext();
        await new ListRepository(ctx).AddAsync(list);
        await new TaskRepository(ctx).AddAsync(task);
        return (list, task);
    }

    [Fact]
    public async Task MergeAsync_RunningTask_ReturnsBlocked()
    {
        var db = NewDb();
        var (_, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.Running);
        var (svc, proxy) = BuildService(db);

        var result = await svc.MergeAsync(task.Id, "main", false, "msg", CancellationToken.None);

        Assert.Equal("blocked", result.Status);
        Assert.Contains("running", result.ErrorMessage ?? "");
        Assert.Empty(proxy.Calls);
    }

    [Fact]
    public async Task MergeAsync_NoWorktree_ReturnsBlocked()
    {
        var db = NewDb();
        var (_, task) = await SeedListAndTask(db, workingDir: "/tmp", status: TaskStatus.Done);
        var (svc, _) = BuildService(db);

        var result = await svc.MergeAsync(task.Id, "main", false, "msg", CancellationToken.None);

        Assert.Equal("blocked", result.Status);
        Assert.Contains("no worktree", result.ErrorMessage ?? "");
    }

    [Fact]
    public async Task MergeAsync_FfAble_KeepWorktree_SetsMergedAndBroadcasts()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        // Create worktree and make a real commit inside it.
        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "added.txt"), "new\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        var (svc, proxy) = BuildService(db);
        var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);

        var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: false,
            commitMessage: "Merge task", ct: CancellationToken.None);

        Assert.Equal("merged", result.Status);
        Assert.Empty(result.ConflictFiles);

        // Worktree state now Merged, dir and branch still present.
        using var ctx = db.CreateContext();
        var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
        Assert.NotNull(wt);
        Assert.Equal(WorktreeState.Merged, wt!.State);
        Assert.True(Directory.Exists(wtCtx.WorktreePath));

        // Broadcast fired.
        Assert.Contains(proxy.Calls, c => c.Method == "WorktreeUpdated" && c.Args[0] is string s && s == task.Id);

        // added.txt is now on the main branch of the repo.
        Assert.True(File.Exists(Path.Combine(repo.RepoDir, "added.txt")));
    }

    [Fact]
    public async Task MergeAsync_FfAble_RemoveWorktree_CleansEverything()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "feature.txt"), "x\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        var (svc, _) = BuildService(db);
        var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);

        var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: true,
            commitMessage: "Merge", ct: CancellationToken.None);

        Assert.Equal("merged", result.Status);
        Assert.False(Directory.Exists(wtCtx.WorktreePath));

        // Branch must be gone.
        var branches = await new GitService().ListLocalBranchesAsync(repo.RepoDir);
        Assert.DoesNotContain(wtCtx.BranchName, branches);

        // DB state still Merged.
        using var ctx = db.CreateContext();
        var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
        Assert.Equal(WorktreeState.Merged, wt!.State);
    }

    [Fact]
    public async Task MergeAsync_DivergedNonConflicting_ProducesMergeCommit()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "feature.txt"), "feat\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        // Advance main by adding a different file.
        File.WriteAllText(Path.Combine(repo.RepoDir, "main-only.txt"), "main\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "chore: main moved");

        var (svc, _) = BuildService(db);
        var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);

        var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: false,
            commitMessage: "Merge diverged", ct: CancellationToken.None);

        Assert.Equal("merged", result.Status);
        // HEAD must be a merge commit (two parents).
        var parents = GitRepoFixture.RunGit(repo.RepoDir, "rev-list", "--parents", "-n", "1", "HEAD").Trim();
        Assert.True(parents.Split(' ').Length >= 3, $"Expected merge commit, got '{parents}'");
    }

    [Fact]
    public async Task MergeAsync_Conflict_AbortsAndReturnsConflictedFiles()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        // Worktree edits README.md
        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "README.md"), "# from worktree\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        // Main also edits README.md (conflicting).
        File.WriteAllText(Path.Combine(repo.RepoDir, "README.md"), "# from main\n");
        GitRepoFixture.RunGit(repo.RepoDir, "add", "-A");
        GitRepoFixture.RunGit(repo.RepoDir, "commit", "-m", "chore: main edit");
        var mainHeadBefore = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();

        var (svc, proxy) = BuildService(db);
        var currentBranch = await new GitService().GetCurrentBranchAsync(repo.RepoDir);

        var result = await svc.MergeAsync(task.Id, currentBranch, removeWorktree: true,
            commitMessage: "Merge", ct: CancellationToken.None);

        Assert.Equal("conflict", result.Status);
        Assert.Contains("README.md", result.ConflictFiles);

        // Main branch must be restored exactly.
        var mainHeadAfter = GitRepoFixture.RunGit(repo.RepoDir, "rev-parse", "HEAD").Trim();
        Assert.Equal(mainHeadBefore, mainHeadAfter);
        Assert.False(await new GitService().IsMidMergeAsync(repo.RepoDir));

        // Worktree state stays Active (no broadcast).
        using var ctx = db.CreateContext();
        var wt = await new WorktreeRepository(ctx).GetByTaskIdAsync(task.Id);
        Assert.Equal(WorktreeState.Active, wt!.State);
        Assert.DoesNotContain(proxy.Calls, c => c.Method == "WorktreeUpdated");
    }

    [Fact]
    public async Task GetTargetsAsync_ReturnsCurrentAndLocalBranches()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var repo = NewRepo();
        GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/extra");
        var db = NewDb();
        var (_, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var (svc, _) = BuildService(db);
        var targets = await svc.GetTargetsAsync(task.Id, CancellationToken.None);

        Assert.False(string.IsNullOrWhiteSpace(targets.DefaultBranch));
        Assert.Contains("feature/extra", targets.LocalBranches);
        Assert.Contains(targets.DefaultBranch, targets.LocalBranches);
    }

    [Fact]
    public async Task MergeAsync_DirtyWorkingTree_ReturnsBlocked()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;
        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        // Dirty the target working dir.
        File.WriteAllText(Path.Combine(repo.RepoDir, "dirt.txt"), "dirty\n");

        var (svc, _) = BuildService(db);
        var result = await svc.MergeAsync(task.Id, "main", false, "Merge", CancellationToken.None);

        Assert.Equal("blocked", result.Status);
        Assert.Contains("uncommitted", result.ErrorMessage ?? "");
    }

    [Fact]
    public async Task MergeAsync_TargetBranchDifferentFromHead_ChecksOutBeforeMerging()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        // Create a feature branch in the repo (from current HEAD).
        GitRepoFixture.RunGit(repo.RepoDir, "branch", "feature/target");

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "feat.txt"), "data\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        var (svc, _) = BuildService(db);
        // Repo is currently on main/master; request merge into feature/target.
        var result = await svc.MergeAsync(task.Id, "feature/target", removeWorktree: false,
            commitMessage: "Merge into feature", ct: CancellationToken.None);

        Assert.Equal("merged", result.Status);

        // HEAD must now be feature/target.
        var head = await new GitService().GetCurrentBranchAsync(repo.RepoDir);
        Assert.Equal("feature/target", head);

        // The merged file must exist on feature/target.
        Assert.True(File.Exists(Path.Combine(repo.RepoDir, "feat.txt")));
    }

    [Fact]
    public async Task MergeAsync_TargetBranchDoesNotExist_ReturnsBlocked()
    {
        if (!GitRepoFixture.IsGitAvailable()) return;

        var repo = NewRepo();
        var db = NewDb();
        var (list, task) = await SeedListAndTask(db, workingDir: repo.RepoDir, status: TaskStatus.Done);

        var wtMgr = BuildWorktreeManager(db);
        var wtCtx = await wtMgr.CreateAsync(task, list, CancellationToken.None);
        _wtCleanups.Add((repo.RepoDir, wtCtx.WorktreePath));

        File.WriteAllText(Path.Combine(wtCtx.WorktreePath, "x.txt"), "x\n");
        await wtMgr.CommitIfChangedAsync(wtCtx, task, list, CancellationToken.None);

        var (svc, _) = BuildService(db);
        var result = await svc.MergeAsync(task.Id, "nonexistent/branch", removeWorktree: false,
            commitMessage: "Merge", ct: CancellationToken.None);

        Assert.Equal("blocked", result.Status);
        Assert.Contains("switch target branch", result.ErrorMessage ?? "");
    }
}

#region Test doubles

internal sealed record MergeHubCall(string Method, object?[] Args);

internal sealed class MergeRecordingClientProxy : IClientProxy
{
    public readonly List<MergeHubCall> Calls = new();
    public Task SendCoreAsync(string method, object?[] args, CancellationToken cancellationToken = default)
    {
        Calls.Add(new MergeHubCall(method, args));
        return Task.CompletedTask;
    }
}

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

internal sealed class MergeRecordingHubContext : IHubContext<ClaudeDo.Worker.Hub.WorkerHub>
{
    private readonly MergeRecordingHubClients _clients = new();
    public MergeRecordingClientProxy Proxy => _clients.AllProxy;
    public IHubClients Clients => _clients;
    public IGroupManager Groups => throw new NotImplementedException();
}

#endregion
