using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Islands;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.Tests.ViewModels;
///
/// [UX-Struktur A2/A4] Group 1's disabled-reason properties (grayed out with an explanation
/// instead of hidden — see task A1) and the hover-or-selected gate for the optional row actions.
///
public class TaskRowContextMenuTests
{
private static TaskRowViewModel MakeRow() => new() { Id = "t1" };
[Fact]
public void SendToQueueDisabledReason_Null_When_Sendable()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
Assert.True(row.CanSendToQueue);
Assert.Null(row.SendToQueueDisabledReason);
}
[Fact]
public void SendToQueueDisabledReason_Set_When_AlreadyQueued()
{
var row = MakeRow();
row.Status = TaskStatus.Queued;
Assert.False(row.CanSendToQueue);
Assert.NotNull(row.SendToQueueDisabledReason);
}
[Fact]
public void SendToQueueDisabledReason_Set_When_Manual()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.IsManual = true;
Assert.False(row.CanSendToQueue);
Assert.NotNull(row.SendToQueueDisabledReason);
}
[Fact]
public void CancelDisabledReason_Null_While_Running()
{
var row = MakeRow();
row.Status = TaskStatus.Running;
Assert.Null(row.CancelDisabledReason);
}
[Fact]
public void CancelDisabledReason_Set_When_NotRunning()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
Assert.NotNull(row.CancelDisabledReason);
}
[Theory]
[InlineData(TaskStatus.Idle)]
[InlineData(TaskStatus.Running)]
[InlineData(TaskStatus.Done)]
public void QuickSessionDisabledReason_Is_Always_Null(TaskStatus status)
{
var row = MakeRow();
row.Status = status;
row.IsManual = true;
Assert.Null(row.QuickSessionDisabledReason);
}
[Fact]
public void RefineDisabledReason_Null_When_Refinable()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
Assert.True(row.CanRefine);
Assert.Null(row.RefineDisabledReason);
}
[Fact]
public void RefineDisabledReason_Set_When_Manual()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.IsManual = true;
Assert.False(row.CanRefine);
Assert.NotNull(row.RefineDisabledReason);
}
[Fact]
public void RefineDisabledReason_Set_When_Refining()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.IsRefining = true;
Assert.False(row.CanRefine);
Assert.NotNull(row.RefineDisabledReason);
}
[Fact]
public void RefineDisabledReason_Set_When_NotIdle()
{
var row = MakeRow();
row.Status = TaskStatus.Running;
Assert.False(row.CanRefine);
Assert.NotNull(row.RefineDisabledReason);
}
[Fact]
public void PlanningDisabledReason_Null_When_OpenAvailable()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
Assert.True(row.CanOpenPlanningSession);
Assert.Null(row.PlanningDisabledReason);
}
[Fact]
public void PlanningDisabledReason_Null_When_PlanningActive()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.PlanningPhase = PlanningPhase.Active;
Assert.True(row.CanResumeOrDiscardPlanning);
Assert.Null(row.PlanningDisabledReason);
}
[Fact]
public void PlanningDisabledReason_Set_When_Finalized()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.PlanningPhase = PlanningPhase.Finalized;
Assert.False(row.CanOpenPlanningSession);
Assert.False(row.CanResumeOrDiscardPlanning);
Assert.False(row.CanFinalizePlanning);
Assert.NotNull(row.PlanningDisabledReason);
}
[Fact]
public void PlanningDisabledReason_Set_When_Manual()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
row.IsManual = true;
Assert.NotNull(row.PlanningDisabledReason);
}
[Theory]
[InlineData(false, false, false)]
[InlineData(true, false, true)]
[InlineData(false, true, true)]
[InlineData(true, true, true)]
public void ShowRowActions_Follows_Hover_Or_Selection(bool isHovered, bool isSelected, bool expected)
{
var row = MakeRow();
row.IsHovered = isHovered;
row.IsSelected = isSelected;
Assert.Equal(expected, row.ShowRowActions);
}
// Pins down what the gate methods guarantee by construction: each of the five CanX/reason
// pairs must agree on every reachable state, not just the handful of examples above. Swept
// across the full state matrix (every TaskStatus x PlanningPhase x IsChild/ParentFinalized x
// IsManual x HasInteractiveSession x HasQueuedSubtasks x IsRefining) rather than one example
// per gate, so a future edit to a gate's condition without touching its Reason branch (or
// vice versa) fails loudly instead of silently drifting.
public static IEnumerable