using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Worker.Tests.Infrastructure;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;

namespace ClaudeDo.Worker.Tests.UiVm;

// ── Fake worker client ────────────────────────────────────────────────────────

sealed class FakeWorkerClient : IWorkerClient
{
    public int StartPlanningCalls { get; private set; }
    public int ResumePlanningCalls { get; private set; }
    public int DiscardPlanningCalls { get; private set; }
    public int FinalizePlanningCalls { get; private set; }
    public int WakeQueueCalls { get; private set; }
    public List<(string TaskId, TaskStatus Status)> SetTaskStatusCalls { get; } = new();

    public bool IsConnected => false;
    public bool IsReconnecting => false;
    public string? LastApproveTarget => null;
#pragma warning disable CS0067 // events required by IWorkerClient but not exercised by this fake
    public event System.ComponentModel.PropertyChangedEventHandler? PropertyChanged;
    public event Action<string, string, DateTime>? TaskStartedEvent;
    public event Action<string, string, string, DateTime>? TaskFinishedEvent;
    public event Action<string>? TaskUpdatedEvent;
    public event Action? ConnectionRestoredEvent;
    public event Action<string>? WorktreeUpdatedEvent;
    public event Action<string>? ListUpdatedEvent;
    public event Action<string, string>? TaskMessageEvent;
    public event Action<WorkerLogEntry>? WorkerLogReceivedEvent;
    public event Action<string, string, string>? TaskQuestionAskedEvent;
    public event Action<string, string>? TaskQuestionResolvedEvent;
    public event Action<string, IReadOnlyList<string>, string>? HandoffRequestedEvent;
    public void RaiseTaskUpdated(string taskId) => TaskUpdatedEvent?.Invoke(taskId);
    public void RaiseWorktreeUpdated(string taskId) => WorktreeUpdatedEvent?.Invoke(taskId);
    public void RaiseTaskMessage(string taskId, string line) => TaskMessageEvent?.Invoke(taskId, line);
    public void RaiseOperationProgress(string opKey, string phase, int current, int total) => OperationProgressEvent?.Invoke(opKey, phase, current, total);

    public Task RunNowAsync(string taskId) => Task.CompletedTask;
    public Task ContinueTaskAsync(string taskId, string followUpPrompt) => Task.CompletedTask;
    public Task AnswerTaskQuestionAsync(string taskId, string questionId, string answer) => Task.CompletedTask;
    public Task<PendingQuestionDto?> GetPendingQuestionAsync(string taskId) => Task.FromResult<PendingQuestionDto?>(null);
    public Task ResetTaskAsync(string taskId) => Task.CompletedTask;
    public Task CancelTaskAsync(string taskId) => Task.CompletedTask;
    public Task<(bool Ok, string? Error)> DeleteTaskAsync(string taskId) => Task.FromResult<(bool, string?)>((true, null));
    public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
    public Task RefreshAgentsAsync() => Task.CompletedTask;
    public Task<SeedResultDto?> RestoreDefaultAgentsAsync() => Task.FromResult<SeedResultDto?>(null);
    public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
    public Task<List<string>> GetRepoImportFoldersAsync() => Task.FromResult(new List<string>());
    public Task SetRepoImportFoldersAsync(List<string> folders) => Task.CompletedTask;
    public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
    public Task<List<SessionSkillDto>> GetSessionSkillsAsync() => Task.FromResult(new List<SessionSkillDto>());
    public Task<List<string>> InstallSessionSkillAsync(string url) => Task.FromResult(new List<string>());
    public Task UpdateSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
    public Task RemoveSessionSkillAsync(string sourceUrl) => Task.CompletedTask;
    public Task<BaseDirtyWarningDto?> SetTaskStatusAsync(string taskId, TaskStatus status)
    {
        SetTaskStatusCalls.Add((taskId, status));
        return Task.FromResult<BaseDirtyWarningDto?>(null);
    }
    public Task SetTaskDoneAsync(string taskId) => Task.CompletedTask;
    public Task UnsetTaskDoneAsync(string taskId) => Task.CompletedTask;
    public Task DequeueTaskAsync(string taskId) => Task.CompletedTask;
    public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
    public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
    public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
    public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
    public Task<MergeConflictDocumentsDto> GetMergeConflictDocumentsAsync(string taskId) => Task.FromResult(new MergeConflictDocumentsDto(taskId, System.Array.Empty<ConflictDocumentDto>()));
    public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
    public Task<MergeResultDto> ContinueConflictMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
    public Task AbortConflictMergeAsync(string taskId) => Task.CompletedTask;
    public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
    public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
    public Task CancelReviewAsync(string taskId) => Task.CompletedTask;
    public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
    public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
    public Task SubmitTaskForReviewAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
    public Task<LaunchSpec> GetInteractiveLaunchSpecAsync(string taskId, CancellationToken ct = default)
        => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
    public Task<LaunchSpec> GetMergeHelperLaunchSpecAsync(IReadOnlyList<string> taskIds, string listId, string handlerTaskId, CancellationToken ct = default)
        => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
    public Task<string> CreateMergeHelperTaskAsync(
        IReadOnlyList<string> taskIds, string listId, string title, string descriptionHeader, CancellationToken ct = default)
        => Task.FromResult(Guid.NewGuid().ToString());
    public Task<LaunchSpec> GetMergeHelperHandoffLaunchSpecAsync(
        string taskId, IReadOnlyList<string> survivingTaskIds, string nextPhase, CancellationToken ct = default)
        => Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>()));
    public Task<LaunchSpec> GetAdHocLaunchSpecAsync(string directory, CancellationToken ct = default)
        => Task.FromResult(new LaunchSpec(directory, "claude", Array.Empty<string>(), new Dictionary<string, string>()));
    public int PlanningStartSpecCalls { get; private set; }
    public int PlanningResumeSpecCalls { get; private set; }
    public Task<LaunchSpec> GetPlanningStartLaunchSpecAsync(string taskId, CancellationToken ct = default)
    { PlanningStartSpecCalls++; return Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>())); }
    public Task<LaunchSpec> GetPlanningResumeLaunchSpecAsync(string taskId, CancellationToken ct = default)
    { PlanningResumeSpecCalls++; return Task.FromResult(new LaunchSpec(".", "claude", Array.Empty<string>(), new Dictionary<string, string>())); }
    public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask;
    public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) { ResumePlanningCalls++; return Task.CompletedTask; }
    public Task<DiscardPlanningOutcome> DiscardPlanningSessionAsync(string taskId, bool dequeueQueuedChildren = false, CancellationToken ct = default)
    {
        DiscardPlanningCalls++;
        return Task.FromResult(new DiscardPlanningOutcome(DiscardPlanningResult.Discarded, 0, 0));
    }
    public Task FinalizePlanningSessionAsync(string taskId, bool queueAgentTasks = true, CancellationToken ct = default) { FinalizePlanningCalls++; return Task.CompletedTask; }
    public Task<int> GetPendingDraftCountAsync(string taskId, CancellationToken ct = default) => Task.FromResult(0);
    public Task<IReadOnlyList<WorkerLogEntry>> GetRecentLogsAsync() => Task.FromResult<IReadOnlyList<WorkerLogEntry>>(System.Array.Empty<WorkerLogEntry>());

    public event Action? NotesUpdatedEvent;
    public event Action? PrepStartedEvent;
    public event Action<string>? PrepLineEvent;
    public event Action<bool>? PrepFinishedEvent;
    public event Action<string>? RefineStartedEvent;
    public event Action<string, bool, string?>? RefineFinishedEvent;
    public event Action<string, string, int, int>? OperationProgressEvent;
    public event Action<string, string, int>? MergeProgressEvent;
    public event Action<string, string>? PlanningMergeStartedEvent;
    public event Action<string, string>? PlanningSubtaskMergedEvent;
    public event Action<string, string, IReadOnlyList<string>, bool>? PlanningMergeConflictEvent;
    public event Action<string, string?>? PlanningMergeAbortedEvent;
    public event Action<string>? PlanningCompletedEvent;
    public event Action<PrimeFiredEvent>? PrimeFired;
    public event Action<UsageSnapshotDto>? UsageUpdatedEvent;
#pragma warning restore CS0067

    public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId) => Task.FromResult<MergeTargetsDto?>(null);
    public Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId) => Task.FromResult<IReadOnlyList<SubtaskDiffDto>>(Array.Empty<SubtaskDiffDto>());
    public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult<CombinedDiffResultDto?>(null);
    public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
    public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask;
    public Task<IReadOnlyList<PlanningMergeConflictStateDto>> GetActiveExternalPlanningMergeConflictsAsync()
        => Task.FromResult<IReadOnlyList<PlanningMergeConflictStateDto>>(Array.Empty<PlanningMergeConflictStateDto>());

    public Task<AppSettingsDto?> GetAppSettingsAsync() => Task.FromResult<AppSettingsDto?>(null);
    public Task UpdateAppSettingsAsync(AppSettingsDto dto) => Task.CompletedTask;
    public Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync() => Task.FromResult(new List<PrimeScheduleDto>());
    public Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto) => Task.FromResult<PrimeScheduleDto?>(null);
    public Task DeletePrimeScheduleAsync(Guid id) => Task.CompletedTask;
    public Task UpdateListAsync(UpdateListDto dto) => Task.CompletedTask;
    public Task UpdateListConfigAsync(UpdateListConfigDto dto) => Task.CompletedTask;
    public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null) => Task.FromResult<WorktreeCleanupDto?>(null);
    public Task<WorktreeResetDto?> ResetAllWorktreesAsync() => Task.FromResult<WorktreeResetDto?>(null);
    public Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId) => Task.FromResult(new List<WorktreeOverviewDto>());
    public Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState) => Task.FromResult((true, (string?)null));
    public Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId) => Task.FromResult<ForceRemoveResultDto?>(null);
    public Task<string?> GetWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult<string?>(null);
    public Task<string> GenerateWeekReportAsync(DateOnly start, DateOnly end) => Task.FromResult("");
    public Task<bool> RunDailyPrepNowAsync() => Task.FromResult(false);
    public Task ClearMyDayAsync() => Task.CompletedTask;
    public Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
    public Task<List<DailyNoteDto>> GetDailyNotesBetweenAsync(DateOnly start, DateOnly end) => Task.FromResult(new List<DailyNoteDto>());
    public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(null);
    public Task UpdateDailyNoteAsync(string id, string text) => Task.CompletedTask;
    public Task DeleteDailyNoteAsync(string id) => Task.CompletedTask;
    public Task<string> GetLastPrepLogAsync() => Task.FromResult(string.Empty);
    public Task<WorkerBuildInfoDto?> GetWorkerBuildInfoAsync() => Task.FromResult<WorkerBuildInfoDto?>(null);
    public Task RefineTaskAsync(string taskId) => Task.CompletedTask;
    public Task<ClaudeBinDto?> GetClaudeBinAsync() => Task.FromResult<ClaudeBinDto?>(null);
    public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value) => Task.FromResult<ClaudeBinDto?>(null);
    public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync() => Task.FromResult<OnlineInboxStateDto?>(null);
    public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask;
    public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask;
    public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask;
    public IReadOnlyList<ActiveTaskDto> GetActiveTasks() => System.Array.Empty<ActiveTaskDto>();

    public Task<UsageSnapshotDto?> GetUsageSnapshotAsync() => Task.FromResult<UsageSnapshotDto?>(null);
    public Task<UsageSnapshotDto?> RefreshUsageAsync() => Task.FromResult<UsageSnapshotDto?>(null);
    public Task<IReadOnlyList<ModelUsageRowDto>> GetModelUsageAsync(DateOnly from, DateOnly to)
        => Task.FromResult<IReadOnlyList<ModelUsageRowDto>>(System.Array.Empty<ModelUsageRowDto>());
    public Task<IReadOnlyList<TaskUsageRowDto>> GetTaskUsageAsync(DateOnly from, DateOnly to)
        => Task.FromResult<IReadOnlyList<TaskUsageRowDto>>(System.Array.Empty<TaskUsageRowDto>());

    public Task<TokenTrackerStatusDto?> GetTokenTrackerStatusAsync() =>
        Task.FromResult<TokenTrackerStatusDto?>(new TokenTrackerStatusDto(
            Installed: false, Version: null, NodeOk: true, NodeVersion: "v22.0.0",
            LastFetchedUtc: null, LastError: null, FormatVersion: null, SessionCount: 0));

    public Task<TokenTrackerStatusDto?> RefreshTokenTrackerAsync() => GetTokenTrackerStatusAsync();

    public Task<TokenTrackerStatusDto?> InstallTokenTrackerAsync() => GetTokenTrackerStatusAsync();

    public Task<TokenTrackerDashboardDto?> OpenTokenTrackerDashboardAsync() =>
        Task.FromResult<TokenTrackerDashboardDto?>(new TokenTrackerDashboardDto(false, null, "not configured"));

    public Task<TicketSettingsDto?> GetTicketSettingsAsync() => Task.FromResult<TicketSettingsDto?>(null);
    public Task SetTicketApiBaseUrlAsync(string? baseUrl) => Task.CompletedTask;
    public Task SetTicketTokenAsync(string token) => Task.CompletedTask;
    public Task ClearTicketTokenAsync() => Task.CompletedTask;
    public Task<TicketConnectionDto?> TestTicketConnectionAsync() => Task.FromResult<TicketConnectionDto?>(null);
    public Task<List<TicketProjectDto>> GetTicketProjectsAsync() => Task.FromResult(new List<TicketProjectDto>());
    public Task<TicketImportResultDto> ImportTicketsAsync(string listId) => Task.FromResult(new TicketImportResultDto(0, 0));
}

// ── Helper to build VM with pre-seeded Items ──────────────────────────────────

file static class VmFactory
{
    // Minimal SQLite :memory: factory — never actually called in these tests
    // (we seed Items directly), but required by the VM constructor.
    private static IDbContextFactory<ClaudeDoDbContext> NullDbFactory()
    {
        var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
            .UseSqlite("DataSource=:memory:")
            .Options;
        return new NullDbContextFactory(opts);
    }

    private sealed class NullDbContextFactory(DbContextOptions<ClaudeDoDbContext> opts)
        : IDbContextFactory<ClaudeDoDbContext>
    {
        public ClaudeDoDbContext CreateDbContext() => new(opts);
    }

    public static (TasksIslandViewModel vm, FakeWorkerClient worker) Create(
        IEnumerable<TaskRowViewModel> rows)
    {
        var worker = new FakeWorkerClient();
        var vm = new TasksIslandViewModel(NullDbFactory(), worker);
        foreach (var r in rows)
            vm.Items.Add(r);
        vm.Regroup();
        return (vm, worker);
    }
}

// ── Tests ─────────────────────────────────────────────────────────────────────

public class TasksIslandViewModelPlanningTests
{
    private static TaskRowViewModel MakeRow(
        string id,
        TaskStatus status,
        string? parentId = null,
        int sortOrder = 0,
        PlanningPhase phase = PlanningPhase.None)
        => new TaskRowViewModel { Id = id, Status = status, ParentTaskId = parentId, PlanningPhase = phase };

    // vm.Rows is the flat HeaderRow|TaskRowViewModel union — a row is "in the Open section" when
    // it's present in Rows and the nearest preceding HeaderRow (if any) isn't Overdue/Completed.
    // HasAction uniquely identifies the Completed header (its Clear-completed button).
    private static bool IsVisibleInOpen(TasksIslandViewModel vm, TaskRowViewModel row)
    {
        var idx = vm.Rows.IndexOf(row);
        if (idx < 0) return false;
        for (int i = idx - 1; i >= 0; i--)
            if (vm.Rows[i] is HeaderRow h)
                return !h.IsOverdue && !h.HasAction;
        return true;
    }

    [Fact]
    public void PlanningParentWithChildren_CollapsedByDefault_ToggleExpands()
    {
        var parent = MakeRow("p1", TaskStatus.Idle, phase: PlanningPhase.Active);
        var child1 = MakeRow("c1", TaskStatus.Idle, "p1");
        var child2 = MakeRow("c2", TaskStatus.Idle, "p1");

        var (vm, _) = VmFactory.Create([parent, child1, child2]);

        // Collapsed by default — children hidden, parent still present
        Assert.False(IsVisibleInOpen(vm, child1));
        Assert.False(IsVisibleInOpen(vm, child2));
        Assert.True(IsVisibleInOpen(vm, parent));

        // Expand the parent
        vm.ToggleExpandCommand.Execute(parent);

        // Children now visible
        Assert.True(IsVisibleInOpen(vm, child1));
        Assert.True(IsVisibleInOpen(vm, child2));
    }

    [Fact]
    public void OpenPlanningSession_IgnoresNonIdleRow()
    {
        var row = MakeRow("t1", TaskStatus.Queued);
        var (vm, _) = VmFactory.Create([row]);
        (string TaskId, bool Resume)? raised = null;
        vm.OpenPlanningConPtyRequested += (id, resume) => raised = (id, resume);

        vm.OpenPlanningSessionCommand.Execute(row);

        Assert.Null(raised);
    }

    [Fact]
    public void OpenPlanningSession_RequestsConPtyPaneForIdleRow()
    {
        var row = MakeRow("t1", TaskStatus.Idle);
        var (vm, _) = VmFactory.Create([row]);
        (string TaskId, bool Resume)? raised = null;
        vm.OpenPlanningConPtyRequested += (id, resume) => raised = (id, resume);

        vm.OpenPlanningSessionCommand.Execute(row);

        Assert.Equal(("t1", false), raised);
    }

    [Fact]
    public void OperationProgress_CreatingWorktreePhase_SetsOnlyMatchingRow()
    {
        var row1 = MakeRow("t1", TaskStatus.Running);
        var row2 = MakeRow("t2", TaskStatus.Running);
        var (_, worker) = VmFactory.Create([row1, row2]);

        worker.RaiseOperationProgress("t1", "creating_worktree", 0, 0);

        Assert.True(row1.HasCreationPhase);
        Assert.False(row2.HasCreationPhase);
    }

    [Fact]
    public void OperationProgress_UnrelatedPhase_DoesNotSetCreationPhase()
    {
        // Same channel carries TaskMergeService's merge phases (opKey is also the task id) --
        // the row must only react to its own creation-phase token, not any phase for its id.
        var row = MakeRow("t1", TaskStatus.Running);
        var (_, worker) = VmFactory.Create([row]);

        worker.RaiseOperationProgress("t1", "merging", 0, 0);

        Assert.False(row.HasCreationPhase);
    }

    [Fact]
    public void ToggleExpand_TogglesParentExpansion()
    {
        var parent = MakeRow("p1", TaskStatus.Idle, phase: PlanningPhase.Finalized);
        var child = MakeRow("c1", TaskStatus.Idle, "p1");

        var (vm, _) = VmFactory.Create([parent, child]);

        // Collapsed by default
        Assert.False(IsVisibleInOpen(vm, child));

        // Expand
        vm.ToggleExpandCommand.Execute(parent);
        Assert.True(IsVisibleInOpen(vm, child));

        // Collapse again
        vm.ToggleExpandCommand.Execute(parent);
        Assert.False(IsVisibleInOpen(vm, child));
    }

    [Fact]
    public void Regroup_ChildWithoutParentInView_ReadsAsTopLevelOrphan()
    {
        // Parent not in the view: the child must be flagged orphan so it renders flat, and it
        // surfaces as a normal top-level row rather than an indented Draft.
        var orphan = MakeRow("c1", TaskStatus.Idle, parentId: "missing-parent");

        var (vm, _) = VmFactory.Create([orphan]);

        Assert.False(orphan.ParentInView);
        Assert.False(orphan.ShowAsChild);
        Assert.False(orphan.IsDraft);
        Assert.True(IsVisibleInOpen(vm, orphan));
    }

    [Fact]
    public void Regroup_ChildWithParentPresent_KeepsParentInView()
    {
        var parent = MakeRow("p1", TaskStatus.Idle, phase: PlanningPhase.Finalized);
        var child = MakeRow("c1", TaskStatus.Idle, "p1");

        var (_, _) = VmFactory.Create([parent, child]);

        Assert.True(child.ParentInView);
        Assert.True(child.ShowAsChild);
    }

    [Fact]
    public async Task SendToQueueAsync_RoutesThroughWorkerHub_NotRawEf()
    {
        var row = MakeRow("t1", TaskStatus.Idle);
        var (vm, worker) = VmFactory.Create([row]);

        await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.SendToQueueCommand).ExecuteAsync(row);

        Assert.Equal(("t1", TaskStatus.Queued), Assert.Single(worker.SetTaskStatusCalls));
    }

    [Fact]
    public async Task SendToQueueAsync_TaskHasOpenInteractiveSession_DoesNotQueue()
    {
        var row = MakeRow("t1", TaskStatus.Idle);
        row.HasInteractiveSession = true;
        var (vm, worker) = VmFactory.Create([row]);

        await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.SendToQueueCommand).ExecuteAsync(row);

        Assert.Empty(worker.SetTaskStatusCalls);
    }
}

// ── My Day add / remove (real DB) ─────────────────────────────────────────────

public sealed class TasksIslandViewModelMyDayTests : IDisposable
{
    private readonly DbFixture _db = new();
    private int _numberSeed;
    public void Dispose() => _db.Dispose();

    [Fact]
    public async Task AddToMyDay_SetsIsMyDay()
    {
        var listId = Guid.NewGuid().ToString("N");
        var taskId = Guid.NewGuid().ToString("N");
        await using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = taskId, ListId = listId, Title = "T", CreatedAt = DateTime.UtcNow,
                Number = ++_numberSeed,
                Status = TaskStatus.Idle, IsMyDay = false,
            });
            await ctx.SaveChangesAsync();
        }

        var vm = new TasksIslandViewModel(_db.CreateFactory(), new FakeWorkerClient());
        var row = new TaskRowViewModel { Id = taskId, Status = TaskStatus.Idle, IsMyDay = false };

        await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.AddToMyDayCommand).ExecuteAsync(row);

        await using var verify = _db.CreateContext();
        var loaded = await verify.Tasks.FirstAsync(t => t.Id == taskId);
        Assert.True(loaded.IsMyDay);
        Assert.True(row.IsMyDay);
    }

    [Fact]
    public async Task RemoveFromMyDay_OnParent_CascadesToEveryChild()
    {
        var listId   = Guid.NewGuid().ToString("N");
        var parentId = Guid.NewGuid().ToString("N");
        var child1   = Guid.NewGuid().ToString("N");
        var child2   = Guid.NewGuid().ToString("N");

        await using (var ctx = _db.CreateContext())
        {
            ctx.Lists.Add(new ListEntity { Id = listId, Name = "L", CreatedAt = DateTime.UtcNow });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = parentId, ListId = listId, Title = "P", CreatedAt = DateTime.UtcNow,
                Number = ++_numberSeed,
                Status = TaskStatus.WaitingForChildren, PlanningPhase = PlanningPhase.Finalized, IsMyDay = true,
            });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = child1, ListId = listId, Title = "C1", CreatedAt = DateTime.UtcNow,
                Number = ++_numberSeed,
                Status = TaskStatus.Idle, ParentTaskId = parentId, IsMyDay = true,
            });
            ctx.Tasks.Add(new TaskEntity
            {
                Id = child2, ListId = listId, Title = "C2", CreatedAt = DateTime.UtcNow,
                Number = ++_numberSeed,
                Status = TaskStatus.Idle, ParentTaskId = parentId, IsMyDay = true,
            });
            await ctx.SaveChangesAsync();
        }

        var vm = new TasksIslandViewModel(_db.CreateFactory(), new FakeWorkerClient());
        var parentRow = new TaskRowViewModel { Id = parentId, Status = TaskStatus.WaitingForChildren, IsMyDay = true };

        await ((IAsyncRelayCommand<TaskRowViewModel?>)vm.RemoveFromMyDayCommand).ExecuteAsync(parentRow);

        await using var verify = _db.CreateContext();
        Assert.False(await verify.Tasks.AnyAsync(t => t.IsMyDay),
            "removing the parent from My Day must clear IsMyDay on the parent and all its children");
    }
}
