using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class DetailsIslandReviewActionsTests : IDisposable
{
    private readonly string _dbPath;

    public DetailsIslandReviewActionsTests()
    {
        _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_review_actions_test_{Guid.NewGuid():N}.db");
        using var ctx = NewContext();
        ctx.Database.EnsureCreated();
    }

    public void Dispose()
    {
        try { File.Delete(_dbPath); } catch { }
        try { File.Delete(_dbPath + "-wal"); } catch { }
        try { File.Delete(_dbPath + "-shm"); } catch { }
    }

    private ClaudeDoDbContext NewContext()
    {
        var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
            .UseSqlite($"Data Source={_dbPath}")
            .Options;
        return new ClaudeDoDbContext(opts);
    }




    private sealed class RecordingWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public string? ParkedTaskId;
        public override Task RejectReviewToIdleAsync(string taskId)
        {
            ParkedTaskId = taskId;
            return Task.CompletedTask;
        }
    }

    private DetailsIslandViewModel BuildVm(StubWorkerClient worker)
    {
        var factory = new TestDbFactory(NewContext);
        return new DetailsIslandViewModel(factory, worker, new NullServiceProvider(), new ClaudeDo.Ui.Services.MergeCoordinator());
    }

    [Fact]
    public async Task ParkReview_CallsRejectReviewToIdle_ForTheBoundTask()
    {
        var worker = new RecordingWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-park-1", Status = TaskStatus.WaitingForReview });

        await vm.ParkReviewCommand.ExecuteAsync(null);

        Assert.Equal("task-park-1", worker.ParkedTaskId);
    }

    [Fact]
    public void SendBack_IsDisabledUntilFeedbackIsEntered()
    {
        var vm = BuildVm(new RecordingWorkerClient());
        vm.Bind(new TaskRowViewModel { Id = "task-fb-1", Status = TaskStatus.WaitingForReview });

        Assert.False(vm.RejectReviewCommand.CanExecute(null));

        vm.ReviewFeedback = "tighten the error handling";
        Assert.True(vm.RejectReviewCommand.CanExecute(null));

        vm.ReviewFeedback = "   ";
        Assert.False(vm.RejectReviewCommand.CanExecute(null));
    }

    [Fact]
    public void Approve_IsEnabled_WhenThereIsNothingToReview()
    {
        // A childless sandbox run has no worktree diff to inspect, so the gate must
        // not block it — it approves straight through.
        var vm = BuildVm(new RecordingWorkerClient());
        vm.Bind(new TaskRowViewModel { Id = "task-nodiff-1", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        Assert.False(vm.Merge.HasReviewableDiff);
        Assert.False(vm.ShowReviewDiffHint);
        Assert.True(vm.ApproveReviewCommand.CanExecute(null));
    }

    [Fact]
    public void Approve_IsGatedUntilDiffOpened_AndReLocksOnNewRun()
    {
        var vm = BuildVm(new RecordingWorkerClient());
        vm.Bind(new TaskRowViewModel { Id = "task-diff-1", Status = TaskStatus.WaitingForReview });
        vm.Merge.SyncWorktree("/tmp/wt", null, null, "Active", null);
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        // There is a diff to read, but it has not been opened → merge is blocked.
        Assert.True(vm.Merge.HasReviewableDiff);
        Assert.True(vm.ShowReviewDiffHint);
        Assert.False(vm.ApproveReviewCommand.CanExecute(null));

        // Opening the diff records the inspection and unlocks the merge.
        vm.Merge.DiffViewed?.Invoke();
        Assert.False(vm.ShowReviewDiffHint);
        Assert.True(vm.ApproveReviewCommand.CanExecute(null));

        // A new run means a fresh diff — the gate re-engages.
        vm.Monitor.ApplyState(TaskStatus.Running);
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);
        Assert.True(vm.ShowReviewDiffHint);
        Assert.False(vm.ApproveReviewCommand.CanExecute(null));
    }

    [Fact]
    public void ShowMergeDrainHint_IsTrueExactlyWhenMergeIsDraining()
    {
        var vm = BuildVm(new RecordingWorkerClient());
        vm.Bind(new TaskRowViewModel { Id = "task-drain-1", Status = TaskStatus.WaitingForReview });

        Assert.False(vm.ShowMergeDrainHint);
        Assert.True(vm.CancelReviewCommand.CanExecute(null));

        vm.IsMergeDraining = true;
        Assert.True(vm.ShowMergeDrainHint);
        Assert.False(vm.CancelReviewCommand.CanExecute(null));

        vm.IsMergeDraining = false;
        Assert.False(vm.ShowMergeDrainHint);
        Assert.True(vm.CancelReviewCommand.CanExecute(null));
    }

    private sealed class ThrowingWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public string ExceptionMessage { get; init; } = "blocked: target working tree has uncommitted changes";
        public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) =>
            throw new Exception(ExceptionMessage);
    }

    [Fact]
    public async Task ApproveReview_WhenWorkerThrows_CallsShowErrorAsync_AndResetsIsRunning()
    {
        var worker = new ThrowingWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-err-1", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        string? reportedError = null;
        vm.ShowErrorAsync = msg => { reportedError = msg; return Task.CompletedTask; };

        await vm.ApproveReviewCommand.ExecuteAsync(null);

        Assert.Equal(worker.ExceptionMessage, reportedError);
        Assert.False(vm.ApproveOp.IsRunning);
        Assert.True(vm.ApproveReviewCommand.CanExecute(null));
    }

    private sealed class BlockingApproveWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public TaskCompletionSource<MergeResultDto?> Gate { get; } = new();
        public override Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Gate.Task;
    }

    private static int MergeProgressSubscriberCount(StubWorkerClient worker)
    {
        var field = typeof(StubWorkerClient).GetField("MergeProgressEvent",
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        var del = (Delegate?)field!.GetValue(worker);
        return del?.GetInvocationList().Length ?? 0;
    }

    [Fact]
    public async Task ApproveReview_SetsIsRunning_AndLocksTheButton_WhileThePendingCallIsInFlight()
    {
        var worker = new BlockingApproveWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-approve-1", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        var approve = vm.ApproveReviewCommand.ExecuteAsync(null);

        Assert.True(vm.ApproveOp.IsRunning);
        Assert.False(vm.ApproveReviewCommand.CanExecute(null));

        worker.Gate.SetResult(new MergeResultDto("merged", new List<string>(), null));
        await approve;

        Assert.False(vm.ApproveOp.IsRunning);
        Assert.True(vm.ApproveReviewCommand.CanExecute(null));
    }

    [Fact]
    public async Task ApproveReview_SharpensLabelForItsOwnTask_ButIgnoresProgressForOtherTasks()
    {
        var worker = new BlockingApproveWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-approve-2", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        var approve = vm.ApproveReviewCommand.ExecuteAsync(null);
        var merging = vm.ApproveOp.Label;

        worker.RaiseMergeProgress("some-other-task", "verifying", 30);
        Assert.Equal(merging, vm.ApproveOp.Label);

        worker.RaiseMergeProgress("task-approve-2", "verifying", 30);
        Assert.NotEqual(merging, vm.ApproveOp.Label);

        worker.Gate.SetResult(new MergeResultDto("merged", new List<string>(), null));
        await approve;
    }

    [Fact]
    public async Task ApproveReview_UnsubscribesFromMergeProgress_AfterTheCallCompletes()
    {
        var worker = new BlockingApproveWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-approve-3", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        var approve = vm.ApproveReviewCommand.ExecuteAsync(null);
        Assert.Equal(1, MergeProgressSubscriberCount(worker));

        worker.Gate.SetResult(new MergeResultDto("merged", new List<string>(), null));
        await approve;

        Assert.Equal(0, MergeProgressSubscriberCount(worker));
    }

    private static int OperationProgressSubscriberCount(StubWorkerClient worker)
    {
        var field = typeof(StubWorkerClient).GetField("OperationProgressEvent",
            System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        var del = (Delegate?)field!.GetValue(worker);
        return del?.GetInvocationList().Length ?? 0;
    }

    [Fact]
    public async Task ApproveReview_ShowsRebasePhase_WithBranchCount_InsteadOfStalledMerging()
    {
        var worker = new BlockingApproveWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-approve-rebase", Status = TaskStatus.WaitingForReview });
        vm.Monitor.ApplyState(TaskStatus.WaitingForReview);

        var approve = vm.ApproveReviewCommand.ExecuteAsync(null);
        var merging = vm.ApproveOp.Label;

        // The rebase phase needs the branch total, which only rides OperationProgress
        // (MergeProgressEvent forwards elapsed-seconds only) — ignored for other tasks.
        worker.RaiseOperationProgress("some-other-task", "rebasing", 1, 3);
        Assert.Equal(merging, vm.ApproveOp.Label);

        worker.RaiseOperationProgress("task-approve-rebase", "rebasing", 2, 3);
        Assert.NotEqual(merging, vm.ApproveOp.Label);
        Assert.Contains("(2/3)", vm.ApproveOp.Label);

        worker.Gate.SetResult(new MergeResultDto("merged", new List<string>(), null));
        await approve;

        Assert.Equal(0, OperationProgressSubscriberCount(worker));
    }

    private sealed class BlockingSubmitWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public TaskCompletionSource Gate { get; } = new();
        public override Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Gate.Task;
    }

    [Fact]
    public async Task SubmitForReview_SetsIsRunning_AndLocksTheButton_WhilePending()
    {
        var worker = new BlockingSubmitWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-submit-1", Status = TaskStatus.Idle });
        vm.Monitor.ApplyState(TaskStatus.Idle);
        vm.WorktreePath = "/tmp/wt";

        var submit = vm.SubmitForReviewCommand.ExecuteAsync(null);

        Assert.True(vm.SubmitOp.IsRunning);
        Assert.False(vm.SubmitForReviewCommand.CanExecute(null));

        worker.Gate.SetResult();
        await submit;

        Assert.False(vm.SubmitOp.IsRunning);
        Assert.True(vm.SubmitForReviewCommand.CanExecute(null));
    }

    private sealed class BlockingRejectWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public TaskCompletionSource Gate { get; } = new();
        public override Task RejectReviewToQueueAsync(string taskId, string feedback) => Gate.Task;
    }

    [Fact]
    public async Task RejectReview_SetsIsRunning_AndLocksTheButton_WhilePending()
    {
        var worker = new BlockingRejectWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-reject-1", Status = TaskStatus.WaitingForReview });
        vm.ReviewFeedback = "tighten the error handling";

        var reject = vm.RejectReviewCommand.ExecuteAsync(null);

        Assert.True(vm.RejectOp.IsRunning);
        Assert.False(vm.RejectReviewCommand.CanExecute(null));

        worker.Gate.SetResult();
        await reject;

        Assert.False(vm.RejectOp.IsRunning);
    }

    private sealed class BlockingParkWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public TaskCompletionSource Gate { get; } = new();
        public override Task RejectReviewToIdleAsync(string taskId) => Gate.Task;
    }

    [Fact]
    public async Task ParkReview_SetsIsRunning_AndLocksTheButton_WhilePending()
    {
        var worker = new BlockingParkWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-park-2", Status = TaskStatus.WaitingForReview });

        var park = vm.ParkReviewCommand.ExecuteAsync(null);

        Assert.True(vm.ParkOp.IsRunning);
        Assert.False(vm.ParkReviewCommand.CanExecute(null));

        worker.Gate.SetResult();
        await park;

        Assert.False(vm.ParkOp.IsRunning);
        Assert.True(vm.ParkReviewCommand.CanExecute(null));
    }

    private sealed class BlockingPreviewWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public TaskCompletionSource<MergePreviewDto?> Gate { get; } = new();
        public override Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Gate.Task;
    }

    [Fact]
    public async Task MergePreview_ShowsBusy_WhileTheWorkerCallIsPending_AndResetsAfter()
    {
        var worker = new BlockingPreviewWorkerClient();
        var vm = BuildVm(worker);
        // SyncTaskContext directly rather than Bind(): Bind() loads the task from the DB
        // asynchronously and TaskId only lands once that completes, which would race the
        // synchronous assertion below.
        vm.Merge.SyncTaskContext("task-preview-1", "do the thing", false);
        vm.Merge.SyncWorktree("/tmp/wt", null, null, "Active", null);

        var preview = vm.Merge.RefreshMergePreviewAsync();

        Assert.True(vm.Merge.PreviewOp.IsRunning);

        worker.Gate.SetResult(new MergePreviewDto("clean", new List<string>(), 3));
        await preview;

        Assert.False(vm.Merge.PreviewOp.IsRunning);
    }

    [Fact]
    public void CancelReview_IsDisabled_WhileUnitMergeIsDraining()
    {
        var worker = new RecordingWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-drain-1", Status = TaskStatus.WaitingForReview });

        Assert.True(vm.CancelReviewCommand.CanExecute(null));

        worker.RaisePlanningMergeStarted("task-drain-1", "main");
        Assert.False(vm.CancelReviewCommand.CanExecute(null));

        worker.RaisePlanningCompleted("task-drain-1");
        Assert.True(vm.CancelReviewCommand.CanExecute(null));
    }

    [Fact]
    public void CancelReview_ReDisables_UntilMergeAborted()
    {
        var worker = new RecordingWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-drain-2", Status = TaskStatus.WaitingForReview });

        worker.RaisePlanningMergeStarted("task-drain-2", "main");
        Assert.False(vm.CancelReviewCommand.CanExecute(null));

        worker.RaisePlanningMergeAborted("task-drain-2");
        Assert.True(vm.CancelReviewCommand.CanExecute(null));
    }

    [Fact]
    public void CancelReview_UnaffectedByMergeEvents_ForADifferentTask()
    {
        var worker = new RecordingWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-drain-3", Status = TaskStatus.WaitingForReview });

        worker.RaisePlanningMergeStarted("some-other-parent", "main");

        Assert.True(vm.CancelReviewCommand.CanExecute(null));
    }

    private sealed class ThrowingCancelWorkerClient : StubWorkerClient
    {
        public override bool IsConnected => true;
        public string ExceptionMessage { get; init; } =
            "A merge is in progress for this task; wait for it to finish before cancelling.";
        public override Task CancelReviewAsync(string taskId) => throw new Exception(ExceptionMessage);
    }

    [Fact]
    public async Task CancelReview_WhenWorkerThrows_RaisesErrorReported()
    {
        var worker = new ThrowingCancelWorkerClient();
        var vm = BuildVm(worker);
        vm.Bind(new TaskRowViewModel { Id = "task-cancel-err-1", Status = TaskStatus.WaitingForReview });

        string? reportedError = null;
        vm.ErrorReported += msg => reportedError = msg;

        await vm.CancelReviewCommand.ExecuteAsync(null);

        Assert.Equal(worker.ExceptionMessage, reportedError);
    }
}
