/**
 * CLI Module Tests - Comprehensive behavioral testing
 *
 * ARCHITECTURE: Tests CLI command parsing, validation, and integration with TaskManager
 * Focus on behavior, not implementation details
 *
 * Coverage target: 500+ lines, 90%+ line coverage
 * Quality: 3-5 assertions per test, AAA pattern, behavioral testing
 */

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import type { ReadOnlyContext } from '../../src/cli/read-only-context';
import { AGENT_PROVIDERS, isAgentProvider } from '../../src/core/agents';
import { loadConfiguration } from '../../src/core/configuration';
import type { Container } from '../../src/core/container';
import type {
  Loop,
  LoopCreateRequest,
  LoopIteration,
  PipelineCreateRequest,
  PipelineResult,
  ResumeTaskRequest,
  Schedule,
  ScheduleCreateRequest,
  ScheduledPipelineCreateRequest,
  ScheduleExecution,
  Task,
  TaskOutput,
  TaskRequest,
} from '../../src/core/domain';
import {
  createLoop,
  createSchedule,
  LoopId,
  LoopStatus,
  LoopStrategy,
  MissedRunPolicy,
  OptimizeDirection,
  Priority,
  ScheduleId,
  ScheduleStatus,
  ScheduleType,
  TaskId,
} from '../../src/core/domain';
import { AutobeatError, ErrorCode, taskNotFound } from '../../src/core/errors';
import { InMemoryEventBus } from '../../src/core/events/event-bus';
import type {
  OutputCapturedEvent,
  TaskCancelledEvent,
  TaskCompletedEvent,
  TaskFailedEvent,
  TaskTimeoutEvent,
} from '../../src/core/events/events';
import type {
  LoopRepository,
  LoopService,
  OutputRepository,
  ScheduleRepository,
  ScheduleService,
  TaskManager,
  TaskRepository,
} from '../../src/core/interfaces';
import { err, ok, type Result } from '../../src/core/result';
import { toMissedRunPolicy, toOptimizeDirection } from '../../src/utils/format';
import { TaskFactory } from '../fixtures/factories';

// Test constants
const VALID_PROMPT = 'analyze the codebase';
const VALID_TASK_ID = 'task-abc123';
const VALID_PRIORITY = 'P0' as const;
const VALID_WORKING_DIR = '/workspace/test';

/**
 * Mock TaskManager for CLI testing
 * Simulates TaskManager behavior without full bootstrap overhead
 */
class MockTaskManager implements TaskManager {
  delegateCalls: TaskRequest[] = [];
  statusCalls: (string | undefined)[] = [];
  logsCalls: Array<{ taskId: string; tail?: number }> = [];
  cancelCalls: Array<{ taskId: string; reason?: string }> = [];
  retryCalls: string[] = [];

  private taskStorage = new Map<string, Task>();

  async delegate(request: TaskRequest) {
    this.delegateCalls.push(request);
    const task = new TaskFactory()
      .withPrompt(request.prompt)
      .withPriority(request.priority || 'P2')
      .build();
    this.taskStorage.set(task.id, task);
    return ok(task);
  }

  async getStatus(taskId?: string) {
    this.statusCalls.push(taskId);
    if (taskId) {
      const task = this.taskStorage.get(taskId);
      return task ? ok(task) : err(taskNotFound(taskId));
    }
    return ok(Array.from(this.taskStorage.values()));
  }

  async getLogs(taskId: string, tail?: number) {
    this.logsCalls.push({ taskId, tail });
    const task = this.taskStorage.get(taskId);
    if (!task) {
      return err(taskNotFound(taskId));
    }
    return ok({
      taskId,
      stdout: ['line 1', 'line 2', 'line 3'],
      stderr: [],
      totalSize: 24,
    });
  }

  async cancel(taskId: string, reason?: string) {
    this.cancelCalls.push({ taskId, reason });
    const task = this.taskStorage.get(taskId);
    if (!task) {
      return err(taskNotFound(taskId));
    }
    task.status = 'cancelled';
    return ok(undefined);
  }

  async retry(taskId: string) {
    this.retryCalls.push(taskId);
    const oldTask = this.taskStorage.get(taskId);
    if (!oldTask) {
      return err(taskNotFound(taskId));
    }
    const newTask = new TaskFactory().withPrompt(oldTask.prompt).build();
    this.taskStorage.set(newTask.id, newTask);
    return ok(newTask);
  }

  resumeCalls: ResumeTaskRequest[] = [];

  async resume(request: ResumeTaskRequest) {
    this.resumeCalls.push(request);
    const oldTask = this.taskStorage.get(request.taskId);
    if (!oldTask) {
      return err(taskNotFound(request.taskId));
    }
    if (oldTask.status !== 'completed' && oldTask.status !== 'failed' && oldTask.status !== 'cancelled') {
      return err(
        new AutobeatError(
          ErrorCode.INVALID_OPERATION,
          `Task ${request.taskId} cannot be resumed in state ${oldTask.status}`,
        ),
      );
    }
    const newTask = new TaskFactory().withPrompt(`PREVIOUS TASK CONTEXT:\n${oldTask.prompt}`).build();
    // biome-ignore lint/suspicious/noExplicitAny: test mock needs to set readonly fields for resume metadata verification
    (newTask as any).retryCount = 1;
    // biome-ignore lint/suspicious/noExplicitAny: test mock needs to set readonly fields for resume metadata verification
    (newTask as any).parentTaskId = request.taskId;
    this.taskStorage.set(newTask.id, newTask);
    return ok(newTask);
  }

  reset() {
    this.delegateCalls = [];
    this.statusCalls = [];
    this.logsCalls = [];
    this.cancelCalls = [];
    this.retryCalls = [];
    this.resumeCalls = [];
    this.taskStorage.clear();
  }
}

/**
 * Mock ScheduleService for CLI schedule command testing
 */
class MockScheduleService implements ScheduleService {
  createCalls: ScheduleCreateRequest[] = [];
  listCalls: Array<{ status?: ScheduleStatus; limit?: number; offset?: number }> = [];
  getCalls: Array<{ scheduleId: string; includeHistory?: boolean; historyLimit?: number }> = [];
  cancelCalls: Array<{ scheduleId: string; reason?: string; cancelTasks?: boolean }> = [];
  pauseCalls: Array<{ scheduleId: string }> = [];
  resumeCalls: Array<{ scheduleId: string }> = [];

  private scheduleStorage = new Map<string, Schedule>();

  async createSchedule(request: ScheduleCreateRequest) {
    this.createCalls.push(request);
    const schedule = createSchedule({
      taskTemplate: {
        prompt: request.prompt,
        priority: request.priority,
        workingDirectory: request.workingDirectory,
      },
      scheduleType: request.scheduleType,
      cronExpression: request.cronExpression,
      scheduledAt: request.scheduledAt ? Date.parse(request.scheduledAt) : undefined,
      timezone: request.timezone ?? 'UTC',
      missedRunPolicy: request.missedRunPolicy ?? MissedRunPolicy.SKIP,
      maxRuns: request.maxRuns,
      expiresAt: request.expiresAt ? Date.parse(request.expiresAt) : undefined,
      afterScheduleId: request.afterScheduleId,
    });
    this.scheduleStorage.set(schedule.id, schedule);
    return ok(schedule);
  }

  createPipelineCalls: PipelineCreateRequest[] = [];
  createScheduledPipelineCalls: ScheduledPipelineCreateRequest[] = [];

  async createPipeline(request: PipelineCreateRequest): Promise<ReturnType<ScheduleService['createPipeline']>> {
    this.createPipelineCalls.push(request);
    const steps = request.steps.map((step, index) => ({
      index,
      scheduleId: ScheduleId(`schedule-step-${index}`),
      prompt: step.prompt,
    }));
    return ok({
      pipelineId: ScheduleId('schedule-step-0'),
      steps,
    } as PipelineResult);
  }

  async createScheduledPipeline(request: ScheduledPipelineCreateRequest) {
    this.createScheduledPipelineCalls.push(request);
    const schedule = createSchedule({
      taskTemplate: {
        prompt: request.steps.map((s) => s.prompt).join(' | '),
        priority: request.priority,
        workingDirectory: request.workingDirectory,
      },
      scheduleType: request.scheduleType,
      cronExpression: request.cronExpression,
      scheduledAt: request.scheduledAt ? Date.parse(request.scheduledAt) : undefined,
      timezone: request.timezone ?? 'UTC',
      missedRunPolicy: request.missedRunPolicy ?? MissedRunPolicy.SKIP,
      maxRuns: request.maxRuns,
      expiresAt: request.expiresAt ? Date.parse(request.expiresAt) : undefined,
      afterScheduleId: request.afterScheduleId,
      pipelineSteps: request.steps,
    });
    this.scheduleStorage.set(schedule.id, schedule);
    return ok(schedule);
  }

  async listSchedules(status?: ScheduleStatus, limit?: number, offset?: number) {
    this.listCalls.push({ status, limit, offset });
    const all = Array.from(this.scheduleStorage.values());
    if (status) {
      return ok(all.filter((s) => s.status === status));
    }
    return ok(all);
  }

  async getSchedule(scheduleId: string, includeHistory?: boolean, historyLimit?: number) {
    this.getCalls.push({ scheduleId, includeHistory, historyLimit });
    const schedule = this.scheduleStorage.get(scheduleId);
    if (!schedule) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Schedule ${scheduleId} not found`));
    }
    const history: ScheduleExecution[] | undefined = includeHistory ? [] : undefined;
    return ok({ schedule, history });
  }

  async cancelSchedule(scheduleId: string, reason?: string, cancelTasks?: boolean) {
    this.cancelCalls.push({ scheduleId, reason, cancelTasks });
    const schedule = this.scheduleStorage.get(scheduleId);
    if (!schedule) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Schedule ${scheduleId} not found`));
    }
    return ok(undefined);
  }

  async pauseSchedule(scheduleId: string) {
    this.pauseCalls.push({ scheduleId });
    const schedule = this.scheduleStorage.get(scheduleId);
    if (!schedule) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Schedule ${scheduleId} not found`));
    }
    return ok(undefined);
  }

  async resumeSchedule(scheduleId: string) {
    this.resumeCalls.push({ scheduleId });
    const schedule = this.scheduleStorage.get(scheduleId);
    if (!schedule) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Schedule ${scheduleId} not found`));
    }
    return ok(undefined);
  }

  reset() {
    this.createCalls = [];
    this.createPipelineCalls = [];
    this.createScheduledPipelineCalls = [];
    this.listCalls = [];
    this.getCalls = [];
    this.cancelCalls = [];
    this.pauseCalls = [];
    this.resumeCalls = [];
    this.scheduleStorage.clear();
  }
}

/**
 * Mock LoopService for CLI loop command testing
 */
class MockLoopService implements LoopService {
  createCalls: LoopCreateRequest[] = [];
  getCalls: Array<{ loopId: string; includeHistory?: boolean; historyLimit?: number }> = [];
  listCalls: Array<{ status?: LoopStatus; limit?: number; offset?: number }> = [];
  cancelCalls: Array<{ loopId: string; reason?: string; cancelTasks?: boolean }> = [];
  pauseCalls: Array<{ loopId: string; options?: { force?: boolean } }> = [];
  resumeCalls: Array<{ loopId: string }> = [];

  private loopStorage = new Map<string, Loop>();

  async createLoop(request: LoopCreateRequest) {
    this.createCalls.push(request);
    const loop = createLoop(request, request.workingDirectory ?? '/workspace');
    this.loopStorage.set(loop.id, loop);
    return ok(loop);
  }

  async getLoop(loopId: LoopId, includeHistory?: boolean, historyLimit?: number) {
    this.getCalls.push({ loopId, includeHistory, historyLimit });
    const loop = this.loopStorage.get(loopId);
    if (!loop) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Loop ${loopId} not found`));
    }
    const iterations: LoopIteration[] | undefined = includeHistory ? [] : undefined;
    return ok({ loop, iterations });
  }

  async listLoops(status?: LoopStatus, limit?: number, offset?: number) {
    this.listCalls.push({ status, limit, offset });
    const all = Array.from(this.loopStorage.values());
    if (status) {
      return ok(all.filter((l) => l.status === status));
    }
    return ok(all);
  }

  async cancelLoop(loopId: LoopId, reason?: string, cancelTasks?: boolean) {
    this.cancelCalls.push({ loopId, reason, cancelTasks });
    const loop = this.loopStorage.get(loopId);
    if (!loop) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Loop ${loopId} not found`));
    }
    return ok(undefined);
  }

  async pauseLoop(loopId: LoopId, options?: { force?: boolean }) {
    this.pauseCalls.push({ loopId, options });
    const loop = this.loopStorage.get(loopId);
    if (!loop) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Loop ${loopId} not found`));
    }
    if (loop.status !== LoopStatus.RUNNING) {
      return err(new AutobeatError(ErrorCode.INVALID_OPERATION, `Loop ${loopId} is not running`));
    }
    return ok(undefined);
  }

  async resumeLoop(loopId: LoopId) {
    this.resumeCalls.push({ loopId });
    const loop = this.loopStorage.get(loopId);
    if (!loop) {
      return err(new AutobeatError(ErrorCode.TASK_NOT_FOUND, `Loop ${loopId} not found`));
    }
    return ok(undefined);
  }

  reset() {
    this.createCalls = [];
    this.getCalls = [];
    this.listCalls = [];
    this.cancelCalls = [];
    this.pauseCalls = [];
    this.resumeCalls = [];
    this.loopStorage.clear();
  }
}

/**
 * Mock Container for dependency injection in tests
 */
class MockContainer implements Container {
  private services = new Map<string, unknown>();

  registerValue(key: string, value: unknown) {
    this.services.set(key, value);
  }

  registerSingleton(key: string, factory: () => unknown) {
    // Store factory, resolve lazily
    this.services.set(key, { factory, instance: null });
  }

  get<T>(key: string) {
    const value = this.services.get(key);
    if (!value) {
      return err(new AutobeatError(ErrorCode.DEPENDENCY_INJECTION_FAILED, `Service not found: ${key}`, { key }));
    }

    // Handle singleton factories
    const record = value as { factory?: () => unknown; instance?: unknown };
    if (record.factory) {
      if (!record.instance) {
        record.instance = record.factory();
      }
      return ok(record.instance as T);
    }

    return ok(value as T);
  }

  async resolve<T>(key: string) {
    return this.get<T>(key);
  }
}

/**
 * Mock ReadOnlyContext for CLI read-only command testing
 *
 * Production code (status, logs, schedule list/get) uses withReadOnlyContext()
 * which creates a ReadOnlyContext with taskRepository, outputRepository,
 * scheduleRepository, and close(). These mock repositories mirror the same
 * interfaces used by the production code paths.
 */
class MockReadOnlyContext {
  readonly taskStorage = new Map<string, Task>();
  readonly outputStorage = new Map<string, TaskOutput>();
  readonly scheduleStorage = new Map<string, Schedule>();
  readonly loopStorage = new Map<string, Loop>();

  readonly taskRepository: Pick<TaskRepository, 'findById' | 'findAll'> = {
    findById: async (taskId: string) => {
      const task = this.taskStorage.get(taskId);
      return ok(task ?? null);
    },
    findAll: async () => {
      return ok(Array.from(this.taskStorage.values()));
    },
  };

  readonly outputRepository: Pick<OutputRepository, 'get'> = {
    get: async (taskId: string) => {
      const output = this.outputStorage.get(taskId);
      return ok(output ?? null);
    },
  };

  readonly scheduleRepository: Pick<
    ScheduleRepository,
    'findAll' | 'findByStatus' | 'findById' | 'getExecutionHistory'
  > = {
    findAll: async (limit?: number) => {
      const all = Array.from(this.scheduleStorage.values());
      return ok(limit ? all.slice(0, limit) : all);
    },
    findByStatus: async (status: string, limit?: number) => {
      const filtered = Array.from(this.scheduleStorage.values()).filter((s) => s.status === status);
      return ok(limit ? filtered.slice(0, limit) : filtered);
    },
    findById: async (scheduleId: string) => {
      const schedule = this.scheduleStorage.get(scheduleId);
      return ok(schedule ?? null);
    },
    getExecutionHistory: async (_scheduleId: string, _limit?: number) => {
      return ok([] as readonly ScheduleExecution[]);
    },
  };

  readonly loopRepository: Pick<LoopRepository, 'findById' | 'findAll' | 'findByStatus' | 'getIterations'> = {
    findById: async (id: LoopId) => {
      const loop = this.loopStorage.get(id);
      return ok(loop ?? null);
    },
    findAll: async (limit?: number) => {
      const all = Array.from(this.loopStorage.values());
      return ok(limit ? all.slice(0, limit) : all);
    },
    findByStatus: async (status: LoopStatus, limit?: number) => {
      const filtered = Array.from(this.loopStorage.values()).filter((l) => l.status === status);
      return ok(limit ? filtered.slice(0, limit) : filtered);
    },
    getIterations: async (_loopId: LoopId, _limit?: number) => {
      return ok([] as readonly LoopIteration[]);
    },
  };

  close = vi.fn();

  /** Seed a task into the mock storage */
  addTask(task: Task): void {
    this.taskStorage.set(task.id, task);
  }

  /** Seed task output into the mock storage */
  addOutput(taskId: string, output: TaskOutput): void {
    this.outputStorage.set(taskId, output);
  }

  /** Seed a schedule into the mock storage */
  addSchedule(schedule: Schedule): void {
    this.scheduleStorage.set(schedule.id, schedule);
  }

  /** Seed a loop into the mock storage */
  addLoop(loop: Loop): void {
    this.loopStorage.set(loop.id, loop);
  }

  reset(): void {
    this.taskStorage.clear();
    this.outputStorage.clear();
    this.scheduleStorage.clear();
    this.loopStorage.clear();
    this.close.mockClear();
  }
}

describe('CLI - Command Parsing and Validation', () => {
  let mockTaskManager: MockTaskManager;
  let mockContainer: MockContainer;
  let mockReadOnlyCtx: MockReadOnlyContext;

  beforeEach(() => {
    mockTaskManager = new MockTaskManager();
    mockReadOnlyCtx = new MockReadOnlyContext();
    mockContainer = new MockContainer();
    mockContainer.registerValue('taskManager', mockTaskManager);
    mockContainer.registerValue('logger', {
      info: vi.fn(),
      error: vi.fn(),
      warn: vi.fn(),
      debug: vi.fn(),
      child: vi.fn(() => ({
        info: vi.fn(),
        error: vi.fn(),
        warn: vi.fn(),
        debug: vi.fn(),
      })),
    });
  });

  afterEach(() => {
    mockTaskManager.reset();
    mockReadOnlyCtx.reset();
  });

  describe('Help Command', () => {
    it('should display comprehensive help text with all commands', () => {
      // This test validates help documentation structure
      // We'll verify the help function outputs correct information

      const helpText = getHelpText();

      expect(helpText).toContain('Autobeat');
      expect(helpText).toContain('mcp start');
      expect(helpText).toContain('run');
      expect(helpText).toContain('status');
      expect(helpText).toContain('logs');
      expect(helpText).toContain('cancel');
    });

    it('should show usage examples for common workflows', () => {
      const helpText = getHelpText();

      expect(helpText).toContain('Examples:');
      expect(helpText).toContain('beat run "analyze');
      expect(helpText).toContain('--priority P0');
      expect(helpText).toContain('status abc123');
    });

    it('should document all priority levels (P0, P1, P2)', () => {
      const helpText = getHelpText();

      expect(helpText).toContain('P0');
      expect(helpText).toContain('P1');
      expect(helpText).toContain('P2');
    });
  });

  describe('Config Command', () => {
    it('should show MCP server configuration in JSON format', () => {
      const configText = getConfigText();

      expect(configText).toContain('mcpServers');
      expect(configText).toContain('autobeat');
      expect(configText).toContain('npx');
      expect(configText).toContain('mcp');
      expect(configText).toContain('start');
    });

    it('should include configuration for all supported platforms', () => {
      const configText = getConfigText();

      expect(configText).toContain('macOS');
      expect(configText).toContain('Windows');
      expect(configText).toContain('claude_desktop_config.json');
    });

    it('should show both global and local installation options', () => {
      const configText = getConfigText();

      expect(configText).toContain('global installation');
      expect(configText).toContain('local development');
      expect(configText).toContain('/path/to/autobeat');
    });
  });

  describe('Run Command - Input Validation', () => {
    it('should reject empty prompt with validation error', () => {
      const result = validateRunInput('', {});

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.INVALID_INPUT);
        expect(result.error.message.toLowerCase()).toContain('prompt');
      }
    });

    it('should reject invalid priority values', () => {
      const result = validateRunInput(VALID_PROMPT, {
        priority: 'P5' as RunOptions['priority'],
      });

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.INVALID_INPUT);
        expect(result.error.message.toLowerCase()).toContain('priority');
      }
    });

    it('should accept all valid priority levels (P0, P1, P2)', () => {
      const priorities = ['P0', 'P1', 'P2'] as const;

      for (const priority of priorities) {
        const result = validateRunInput(VALID_PROMPT, { priority });
        expect(result.ok).toBe(true);
      }
    });

    it('should validate working directory path format', () => {
      const invalidPaths = [
        '../../../etc/passwd', // Path traversal
        'relative/path', // Non-absolute
        '/path/with/../../', // Normalized traversal
      ];

      for (const workingDirectory of invalidPaths) {
        const result = validateRunInput(VALID_PROMPT, { workingDirectory });
        expect(result.ok).toBe(false);
        if (!result.ok) {
          expect(result.error.code).toBe(ErrorCode.INVALID_DIRECTORY);
        }
      }
    });

    it('should validate timeout is positive number', () => {
      const invalidTimeouts = [-100, 0, NaN, Infinity];

      for (const timeout of invalidTimeouts) {
        const result = validateRunInput(VALID_PROMPT, { timeout });
        expect(result.ok).toBe(false);
      }
    });

    it('should validate maxOutputBuffer is within limits', () => {
      const result = validateRunInput(VALID_PROMPT, {
        maxOutputBuffer: 1024 * 1024 * 1024 * 10, // 10GB - too large
      });

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.INVALID_INPUT);
      }
    });
  });

  describe('Run Command - Task Creation', () => {
    it('should create task with prompt and default priority P2', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT);

      expect(mockTaskManager.delegateCalls).toHaveLength(1);
      expect(mockTaskManager.delegateCalls[0].prompt).toBe(VALID_PROMPT);
      expect(mockTaskManager.delegateCalls[0].priority).toBe('P2');
    });

    it('should create task with custom priority when specified', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT, {
        priority: VALID_PRIORITY,
      });

      expect(mockTaskManager.delegateCalls).toHaveLength(1);
      expect(mockTaskManager.delegateCalls[0].priority).toBe(VALID_PRIORITY);
    });

    it('should use current directory as default working directory', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT);

      const call = mockTaskManager.delegateCalls[0];
      expect(call.workingDirectory).toBeTruthy();
      expect(call.workingDirectory).toMatch(/^\//); // Absolute path
    });

    it('should use custom working directory when provided', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT, {
        workingDirectory: VALID_WORKING_DIR,
      });

      expect(mockTaskManager.delegateCalls[0].workingDirectory).toBe(VALID_WORKING_DIR);
    });

    it('should return task ID after successful delegation', async () => {
      const result = await simulateRunCommand(mockTaskManager, VALID_PROMPT);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.id).toBeTruthy();
        expect(result.value.prompt).toBe(VALID_PROMPT);
      }
    });
  });

  describe('Run Command - continueFrom Option', () => {
    it('should pass continueFrom when --continue-from is provided', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT, {
        continueFrom: 'task-parent-abc',
        dependsOn: ['task-parent-abc'],
      });

      const call = mockTaskManager.delegateCalls[0];
      expect(call.continueFrom).toBe('task-parent-abc');
      expect(call.dependsOn).toContain('task-parent-abc');
    });

    it('should not include continueFrom when --continue-from is not provided', async () => {
      await simulateRunCommand(mockTaskManager, VALID_PROMPT);

      const call = mockTaskManager.delegateCalls[0];
      expect(call.continueFrom).toBeUndefined();
    });
  });

  describe('Flag Aliases', () => {
    it('should recognize --continue and -c as aliases for --continue-from', () => {
      const longForm = parseRunArgs(['--continue-from', 'task-123', 'do work']);
      const shortName = parseRunArgs(['--continue', 'task-123', 'do work']);
      const shortFlag = parseRunArgs(['-c', 'task-123', 'do work']);

      expect(longForm.continueFrom).toBe('task-123');
      expect(shortName.continueFrom).toBe('task-123');
      expect(shortFlag.continueFrom).toBe('task-123');
    });

    it('should recognize --deps as alias for --depends-on', () => {
      const longForm = parseRunArgs(['--depends-on', 'task-1,task-2', 'do work']);
      const shortForm = parseRunArgs(['--deps', 'task-1,task-2', 'do work']);

      expect(longForm.dependsOn).toEqual(['task-1', 'task-2']);
      expect(shortForm.dependsOn).toEqual(['task-1', 'task-2']);
    });

    it('should recognize -b and --buffer as aliases for --max-output-buffer', () => {
      const longForm = parseRunArgs(['--max-output-buffer', '5242880', 'do work']);
      const shortName = parseRunArgs(['--buffer', '5242880', 'do work']);
      const shortFlag = parseRunArgs(['-b', '5242880', 'do work']);

      expect(longForm.maxOutputBuffer).toBe(5242880);
      expect(shortName.maxOutputBuffer).toBe(5242880);
      expect(shortFlag.maxOutputBuffer).toBe(5242880);
    });
  });

  describe('Status Command - Single Task (ReadOnlyContext)', () => {
    it('should find task by ID via taskRepository.findById()', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);

      const result = await simulateStatusCommand(mockReadOnlyCtx, VALID_TASK_ID);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).not.toBeNull();
        expect(result.value!.id).toBe(VALID_TASK_ID);
      }
    });

    it('should return task with all status fields', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);

      const result = await simulateStatusCommand(mockReadOnlyCtx, VALID_TASK_ID);

      expect(result.ok).toBe(true);
      if (result.ok && result.value) {
        expect(result.value).toHaveProperty('id');
        expect(result.value).toHaveProperty('status');
        expect(result.value).toHaveProperty('prompt');
        expect(result.value).toHaveProperty('priority');
        expect(result.value).toHaveProperty('createdAt');
      }
    });

    it('should return null for non-existent task ID', async () => {
      const result = await simulateStatusCommand(mockReadOnlyCtx, 'non-existent-task');

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBeNull();
      }
    });

    it('should handle task status transitions correctly', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);

      const result = await simulateStatusCommand(mockReadOnlyCtx, VALID_TASK_ID);
      expect(result.ok).toBe(true);
      if (result.ok && result.value) {
        expect(['queued', 'running', 'completed']).toContain(result.value.status);
      }
    });
  });

  describe('Status Command - All Tasks (ReadOnlyContext)', () => {
    it('should list all tasks when no task ID provided', async () => {
      mockReadOnlyCtx.addTask(new TaskFactory().withId('task-1').withPrompt('task 1').build());
      mockReadOnlyCtx.addTask(new TaskFactory().withId('task-2').withPrompt('task 2').build());
      mockReadOnlyCtx.addTask(new TaskFactory().withId('task-3').withPrompt('task 3').build());

      const result = await simulateStatusCommandAll(mockReadOnlyCtx);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(Array.isArray(result.value)).toBe(true);
        expect(result.value.length).toBe(3);
      }
    });

    it('should return empty array when no tasks exist', async () => {
      const result = await simulateStatusCommandAll(mockReadOnlyCtx);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(Array.isArray(result.value)).toBe(true);
        expect(result.value.length).toBe(0);
      }
    });

    it('should include tasks with different statuses in listing', async () => {
      mockReadOnlyCtx.addTask(new TaskFactory().withId('task-1').withPrompt('task 1').build());
      mockReadOnlyCtx.addTask(new TaskFactory().withId('task-2').withPrompt('task 2').running().build());

      const result = await simulateStatusCommandAll(mockReadOnlyCtx);

      expect(result.ok).toBe(true);
      if (result.ok && Array.isArray(result.value)) {
        result.value.forEach((task) => {
          expect(task).toHaveProperty('status');
          expect(['queued', 'running', 'completed', 'failed', 'cancelled']).toContain(task.status);
        });
      }
    });
  });

  describe('Logs Command (ReadOnlyContext)', () => {
    it('should fetch logs via taskRepository.findById() then outputRepository.get()', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);
      mockReadOnlyCtx.addOutput(VALID_TASK_ID, {
        taskId: TaskId(VALID_TASK_ID),
        stdout: ['line 1', 'line 2', 'line 3'],
        stderr: [],
        totalSize: 24,
      });

      const logsResult = await simulateLogsCommand(mockReadOnlyCtx, VALID_TASK_ID);

      expect(logsResult.ok).toBe(true);
      if (logsResult.ok && logsResult.value) {
        expect(logsResult.value.stdout).toEqual(['line 1', 'line 2', 'line 3']);
      }
    });

    it('should return stdout and stderr arrays from outputRepository', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);
      mockReadOnlyCtx.addOutput(VALID_TASK_ID, {
        taskId: TaskId(VALID_TASK_ID),
        stdout: ['output line'],
        stderr: ['error line'],
        totalSize: 30,
      });

      const logsResult = await simulateLogsCommand(mockReadOnlyCtx, VALID_TASK_ID);

      expect(logsResult.ok).toBe(true);
      if (logsResult.ok && logsResult.value) {
        expect(Array.isArray(logsResult.value.stdout)).toBe(true);
        expect(Array.isArray(logsResult.value.stderr)).toBe(true);
        expect(logsResult.value).toHaveProperty('totalSize');
      }
    });

    it('should support tail option to slice output lines', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);
      mockReadOnlyCtx.addOutput(VALID_TASK_ID, {
        taskId: TaskId(VALID_TASK_ID),
        stdout: ['line 1', 'line 2', 'line 3', 'line 4', 'line 5'],
        stderr: [],
        totalSize: 50,
      });

      const tailCount = 2;
      const logsResult = await simulateLogsCommand(mockReadOnlyCtx, VALID_TASK_ID, tailCount);

      expect(logsResult.ok).toBe(true);
      if (logsResult.ok && logsResult.value) {
        // Production code slices: stdoutLines = output.stdout.slice(-tail)
        expect(logsResult.value.stdout).toEqual(['line 4', 'line 5']);
      }
    });

    it('should return null output for non-existent task', async () => {
      const result = await simulateLogsCommand(mockReadOnlyCtx, 'non-existent-task');

      // Production code: taskRepository.findById() returns null -> error exit
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBeNull();
      }
    });

    it('should handle tasks with no output gracefully', async () => {
      const task = new TaskFactory().withId(VALID_TASK_ID).withPrompt(VALID_PROMPT).build();
      mockReadOnlyCtx.addTask(task);
      // No output added — outputRepository.get() returns null

      const logsResult = await simulateLogsCommand(mockReadOnlyCtx, VALID_TASK_ID);

      expect(logsResult.ok).toBe(true);
      if (logsResult.ok) {
        // Task exists but no output -> null output
        expect(logsResult.value).toBeNull();
      }
    });
  });

  describe('Cancel Command', () => {
    it('should cancel task with provided task ID', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, VALID_PROMPT);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const taskId = runResult.value.id;

      const cancelResult = await simulateCancelCommand(mockTaskManager, taskId);

      expect(cancelResult.ok).toBe(true);
      expect(mockTaskManager.cancelCalls).toHaveLength(1);
      expect(mockTaskManager.cancelCalls[0].taskId).toBe(taskId);
    });

    it('should accept optional cancellation reason', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, VALID_PROMPT);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const taskId = runResult.value.id;
      const reason = 'User requested cancellation';

      await simulateCancelCommand(mockTaskManager, taskId, reason);

      expect(mockTaskManager.cancelCalls[0].reason).toBe(reason);
    });

    it('should return error for non-existent task', async () => {
      const result = await simulateCancelCommand(mockTaskManager, 'non-existent-task');

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.TASK_NOT_FOUND);
      }
    });

    it('should update task status to cancelled after cancellation', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, VALID_PROMPT);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const taskId = runResult.value.id;

      await simulateCancelCommand(mockTaskManager, taskId);

      // Verify via TaskManager.getStatus() — cancel is a mutation command, not read-only
      const statusResult = await mockTaskManager.getStatus(taskId);
      expect(statusResult.ok).toBe(true);
      if (statusResult.ok) {
        expect(statusResult.value.status).toBe('cancelled');
      }
    });
  });

  describe('Retry Command', () => {
    it('should retry task with provided task ID', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, VALID_PROMPT);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const taskId = runResult.value.id;

      const retryResult = await simulateRetryCommand(mockTaskManager, taskId);

      expect(retryResult.ok).toBe(true);
      expect(mockTaskManager.retryCalls).toHaveLength(1);
      expect(mockTaskManager.retryCalls[0]).toBe(taskId);
    });

    it('should create new task with same prompt as original', async () => {
      const originalPrompt = 'original task prompt';
      const runResult = await simulateRunCommand(mockTaskManager, originalPrompt);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const taskId = runResult.value.id;

      const retryResult = await simulateRetryCommand(mockTaskManager, taskId);

      expect(retryResult.ok).toBe(true);
      if (retryResult.ok) {
        expect(retryResult.value.prompt).toBe(originalPrompt);
        expect(retryResult.value.id).not.toBe(taskId); // New task, new ID
      }
    });

    it('should return error for non-existent task', async () => {
      const result = await simulateRetryCommand(mockTaskManager, 'non-existent-task');

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.TASK_NOT_FOUND);
      }
    });

    it('should return new task ID after successful retry', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, VALID_PROMPT);
      expect(runResult.ok).toBe(true);

      if (!runResult.ok) return;
      const originalTaskId = runResult.value.id;

      const retryResult = await simulateRetryCommand(mockTaskManager, originalTaskId);

      expect(retryResult.ok).toBe(true);
      if (retryResult.ok) {
        expect(retryResult.value.id).toBeTruthy();
        expect(retryResult.value.id).not.toBe(originalTaskId);
      }
    });
  });
});

// ============================================================================
// Schedule, Pipeline, and Resume Command Tests
// ============================================================================

describe('CLI - Schedule Commands', () => {
  let mockScheduleService: MockScheduleService;
  let mockScheduleReadOnlyCtx: MockReadOnlyContext;
  let parseScheduleCreateArgs: typeof import('../../src/cli/commands/schedule').parseScheduleCreateArgs;

  beforeAll(async () => {
    const mod = await import('../../src/cli/commands/schedule');
    parseScheduleCreateArgs = mod.parseScheduleCreateArgs;
  });

  beforeEach(() => {
    mockScheduleService = new MockScheduleService();
    mockScheduleReadOnlyCtx = new MockReadOnlyContext();
  });

  afterEach(() => {
    mockScheduleService.reset();
    mockScheduleReadOnlyCtx.reset();
  });

  describe('parseScheduleCreateArgs - pure function', () => {
    it('should parse cron schedule', () => {
      const result = parseScheduleCreateArgs(['run', 'tests', '--cron', '0 9 * * *']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.scheduleType).toBe('cron');
      expect(result.value.cronExpression).toBe('0 9 * * *');
      if (result.value.isPipeline) return;
      expect(result.value.prompt).toBe('run tests');
    });

    it('should parse one-time schedule with --at', () => {
      const result = parseScheduleCreateArgs(['deploy', '--at', '2026-04-01T09:00:00Z']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.scheduleType).toBe('one_time');
      expect(result.value.scheduledAt).toBe('2026-04-01T09:00:00Z');
      if (result.value.isPipeline) return;
      expect(result.value.prompt).toBe('deploy');
    });

    it('should infer type from --cron without explicit --type', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '*/5 * * * *']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.scheduleType).toBe('cron');
    });

    it('should infer type from --at without explicit --type', () => {
      const result = parseScheduleCreateArgs(['task', '--at', '2026-04-01T09:00:00Z']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.scheduleType).toBe('one_time');
    });

    it('should parse all optional flags', () => {
      const cwd = process.cwd();
      const result = parseScheduleCreateArgs([
        'run',
        'tests',
        '--cron',
        '0 9 * * 1-5',
        '--timezone',
        'America/New_York',
        '--missed-run-policy',
        'catchup',
        '--priority',
        'P0',
        '--working-directory',
        cwd,
        '--max-runs',
        '10',
        '--expires-at',
        '2026-12-31T23:59:59Z',
        '--after',
        'sched-abc',
        '--agent',
        'claude',
      ]);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.timezone).toBe('America/New_York');
      expect(result.value.missedRunPolicy).toBe('catchup');
      expect(result.value.priority).toBe('P0');
      expect(result.value.workingDirectory).toBe(cwd);
      expect(result.value.maxRuns).toBe(10);
      expect(result.value.expiresAt).toBe('2026-12-31T23:59:59Z');
      expect(result.value.afterScheduleId).toBe('sched-abc');
      expect(result.value.agent).toBe('claude');
    });

    it('should parse pipeline with --pipeline and --step flags', () => {
      const result = parseScheduleCreateArgs(['--pipeline', '--step', 'lint', '--step', 'test', '--cron', '0 9 * * *']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.isPipeline).toBe(true);
      if (!result.value.isPipeline) return;
      expect(result.value.pipelineSteps).toEqual(['lint', 'test']);
    });

    it('should suppress prompt in pipeline mode (matches loop parser)', () => {
      const result = parseScheduleCreateArgs([
        'extra',
        'words',
        '--pipeline',
        '--step',
        'lint',
        '--step',
        'test',
        '--cron',
        '0 9 * * *',
      ]);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.isPipeline).toBe(true);
    });

    it('should parse --priority with -p shorthand', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '-p', 'P1']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.priority).toBe('P1');
    });

    it('should parse --agent with -a shorthand', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '-a', 'claude']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.agent).toBe('claude');
    });

    // Error cases
    it('should reject both --cron and --at', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--at', '2026-04-01T09:00:00Z']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Cannot specify both');
    });

    it('should reject --type conflict with --cron', () => {
      const result = parseScheduleCreateArgs(['task', '--type', 'one_time', '--cron', '0 9 * * *']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('conflicts');
    });

    it('should reject missing schedule type', () => {
      const result = parseScheduleCreateArgs(['task']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--cron');
    });

    it('should reject invalid --type value', () => {
      const result = parseScheduleCreateArgs(['task', '--type', 'weekly']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--type');
    });

    it('should reject invalid --missed-run-policy', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--missed-run-policy', 'ignore']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--missed-run-policy');
    });

    it('should reject invalid priority', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--priority', 'P9']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Priority');
    });

    it('should reject pipeline with fewer than 2 steps', () => {
      const result = parseScheduleCreateArgs(['--pipeline', '--step', 'only-one', '--cron', '0 9 * * *']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('at least 2');
    });

    it('should reject --step without --pipeline', () => {
      const result = parseScheduleCreateArgs(['task', '--step', 'lint', '--step', 'test', '--cron', '0 9 * * *']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--pipeline');
    });

    it('should reject unknown flag', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--bogus']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Unknown flag');
    });

    it('should reject missing prompt in non-pipeline mode', () => {
      const result = parseScheduleCreateArgs(['--cron', '0 9 * * *']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Usage');
    });

    it('should reject non-positive --max-runs', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--max-runs', '0']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--max-runs');
    });

    it('should reject unknown agent', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--agent', 'skynet']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Unknown agent');
    });

    it('should reject --agent without value', () => {
      const result = parseScheduleCreateArgs(['task', '--cron', '0 9 * * *', '--agent', '--priority']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--agent');
    });
  });

  describe('schedule create', () => {
    it('should create a cron schedule with required fields', async () => {
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'run tests',
        type: 'cron',
        cron: '0 9 * * *',
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls).toHaveLength(1);
      expect(mockScheduleService.createCalls[0].prompt).toBe('run tests');
      expect(mockScheduleService.createCalls[0].scheduleType).toBe(ScheduleType.CRON);
      expect(mockScheduleService.createCalls[0].cronExpression).toBe('0 9 * * *');
    });

    it('should create a one-time schedule with scheduledAt', async () => {
      const futureDate = new Date(Date.now() + 86400000).toISOString();
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'deploy',
        type: 'one_time',
        at: futureDate,
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls[0].scheduleType).toBe(ScheduleType.ONE_TIME);
      expect(mockScheduleService.createCalls[0].scheduledAt).toBe(futureDate);
    });

    it('should pass optional parameters through correctly', async () => {
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'run tests',
        type: 'cron',
        cron: '0 9 * * 1-5',
        timezone: 'America/New_York',
        missedRunPolicy: 'catchup',
        priority: 'P0',
        workingDirectory: process.cwd(),
        maxRuns: 10,
      });

      expect(result.ok).toBe(true);
      const call = mockScheduleService.createCalls[0];
      expect(call.timezone).toBe('America/New_York');
      expect(call.missedRunPolicy).toBe(MissedRunPolicy.CATCHUP);
      expect(call.maxRuns).toBe(10);
    });

    it('should pass afterScheduleId for schedule chaining', async () => {
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'second task',
        type: 'cron',
        cron: '0 9 * * *',
        afterScheduleId: 'schedule-abc123',
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls[0].afterScheduleId).toBe(ScheduleId('schedule-abc123'));
    });

    it('should infer type from --cron flag', async () => {
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'run tests',
        type: 'cron',
        cron: '0 9 * * *',
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls[0].scheduleType).toBe(ScheduleType.CRON);
    });

    it('should infer type from --at flag', async () => {
      const futureDate = new Date(Date.now() + 86400000).toISOString();
      const result = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'deploy',
        type: 'one_time',
        at: futureDate,
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls[0].scheduleType).toBe(ScheduleType.ONE_TIME);
    });

    it('should create scheduled pipeline with --pipeline and --step flags', async () => {
      const result = await simulateScheduleCreatePipeline(mockScheduleService, {
        steps: ['lint', 'test'],
        cron: '0 9 * * *',
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createScheduledPipelineCalls).toHaveLength(1);
      const call = mockScheduleService.createScheduledPipelineCalls[0];
      expect(call.steps).toHaveLength(2);
      expect(call.steps[0].prompt).toBe('lint');
      expect(call.steps[1].prompt).toBe('test');
      expect(call.scheduleType).toBe(ScheduleType.CRON);
      expect(call.cronExpression).toBe('0 9 * * *');
    });
  });

  describe('schedule list (ReadOnlyContext)', () => {
    it('should list all schedules via scheduleRepository.findAll()', async () => {
      const schedule = createSchedule({
        taskTemplate: { prompt: 'task 1' },
        scheduleType: ScheduleType.CRON,
        cronExpression: '0 9 * * *',
        timezone: 'UTC',
        missedRunPolicy: MissedRunPolicy.SKIP,
      });
      mockScheduleReadOnlyCtx.addSchedule(schedule);

      const result = await simulateScheduleListCommand(mockScheduleReadOnlyCtx);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.length).toBe(1);
        expect(result.value[0].id).toBe(schedule.id);
      }
    });

    it('should filter by status via scheduleRepository.findByStatus()', async () => {
      const schedule = createSchedule({
        taskTemplate: { prompt: 'active task' },
        scheduleType: ScheduleType.CRON,
        cronExpression: '0 9 * * *',
        timezone: 'UTC',
        missedRunPolicy: MissedRunPolicy.SKIP,
      });
      mockScheduleReadOnlyCtx.addSchedule(schedule);

      const result = await simulateScheduleListCommand(mockScheduleReadOnlyCtx, ScheduleStatus.ACTIVE);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.length).toBe(1);
      }
    });

    it('should return empty array when no schedules match status filter', async () => {
      const result = await simulateScheduleListCommand(mockScheduleReadOnlyCtx, ScheduleStatus.PAUSED);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.length).toBe(0);
      }
    });
  });

  describe('schedule status (ReadOnlyContext)', () => {
    it('should get schedule details by ID via scheduleRepository.findById()', async () => {
      const schedule = createSchedule({
        taskTemplate: { prompt: 'test' },
        scheduleType: ScheduleType.CRON,
        cronExpression: '0 9 * * *',
        timezone: 'UTC',
        missedRunPolicy: MissedRunPolicy.SKIP,
      });
      mockScheduleReadOnlyCtx.addSchedule(schedule);

      const result = await simulateScheduleStatusCommand(mockScheduleReadOnlyCtx, schedule.id);
      expect(result.ok).toBe(true);
      if (result.ok && result.value) {
        expect(result.value.id).toBe(schedule.id);
        expect(result.value.scheduleType).toBe(ScheduleType.CRON);
      }
    });

    it('should return null for non-existent schedule', async () => {
      const result = await simulateScheduleStatusCommand(mockScheduleReadOnlyCtx, ScheduleId('non-existent'));
      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value).toBeNull();
      }
    });
  });

  describe('schedule cancel', () => {
    it('should cancel existing schedule', async () => {
      const createResult = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'test',
        type: 'cron',
        cron: '0 9 * * *',
      });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const result = await mockScheduleService.cancelSchedule(createResult.value.id, 'no longer needed');
      expect(result.ok).toBe(true);
      expect(mockScheduleService.cancelCalls[0].reason).toBe('no longer needed');
    });

    it('should return error for non-existent schedule', async () => {
      const result = await mockScheduleService.cancelSchedule(ScheduleId('non-existent'));
      expect(result.ok).toBe(false);
    });

    it('should pass cancelTasks flag when --cancel-tasks is provided', async () => {
      const createResult = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'test',
        type: 'cron',
        cron: '0 9 * * *',
      });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const result = await simulateScheduleCancel(mockScheduleService, {
        scheduleId: createResult.value.id,
        cancelTasks: true,
      });

      expect(result.ok).toBe(true);
      expect(mockScheduleService.cancelCalls).toHaveLength(1);
      expect(mockScheduleService.cancelCalls[0].cancelTasks).toBe(true);
    });
  });

  describe('schedule pause', () => {
    it('should pause existing schedule', async () => {
      const createResult = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'test',
        type: 'cron',
        cron: '0 9 * * *',
      });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const result = await mockScheduleService.pauseSchedule(createResult.value.id);
      expect(result.ok).toBe(true);
    });
  });

  describe('schedule resume', () => {
    it('should resume existing schedule', async () => {
      const createResult = await simulateScheduleCreate(mockScheduleService, {
        prompt: 'test',
        type: 'cron',
        cron: '0 9 * * *',
      });
      expect(createResult.ok).toBe(true);
      if (!createResult.ok) return;

      const result = await mockScheduleService.resumeSchedule(createResult.value.id);
      expect(result.ok).toBe(true);
    });
  });
});

describe('CLI - Pipeline Command', () => {
  let mockScheduleService: MockScheduleService;

  beforeEach(() => {
    mockScheduleService = new MockScheduleService();
  });

  afterEach(() => {
    mockScheduleService.reset();
  });

  describe('pipeline creation', () => {
    it('should create pipeline with single step', async () => {
      const result = await simulatePipeline(mockScheduleService, ['setup db']);

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls).toHaveLength(1);
      expect(mockScheduleService.createCalls[0].prompt).toBe('setup db');
      expect(mockScheduleService.createCalls[0].scheduleType).toBe(ScheduleType.ONE_TIME);
    });

    it('should create pipeline with multiple chained steps', async () => {
      const result = await simulatePipeline(mockScheduleService, ['setup db', 'run migrations', 'seed data']);

      expect(result.ok).toBe(true);
      expect(mockScheduleService.createCalls).toHaveLength(3);

      // First step has no afterScheduleId
      expect(mockScheduleService.createCalls[0].prompt).toBe('setup db');
      expect(mockScheduleService.createCalls[0].afterScheduleId).toBeUndefined();

      // Second step chains to first
      expect(mockScheduleService.createCalls[1].prompt).toBe('run migrations');
      expect(mockScheduleService.createCalls[1].afterScheduleId).toBeDefined();

      // Third step chains to second
      expect(mockScheduleService.createCalls[2].prompt).toBe('seed data');
      expect(mockScheduleService.createCalls[2].afterScheduleId).toBeDefined();
    });

    it('should reject empty pipeline', () => {
      const validation = validatePipelineInput([]);
      expect(validation.ok).toBe(false);
    });
  });
});

describe('CLI - Resume Command', () => {
  let mockTaskManager: MockTaskManager;

  beforeEach(() => {
    mockTaskManager = new MockTaskManager();
  });

  afterEach(() => {
    mockTaskManager.reset();
  });

  describe('resume', () => {
    it('should resume a failed task', async () => {
      // Create and fail a task first
      const runResult = await simulateRunCommand(mockTaskManager, 'original task');
      expect(runResult.ok).toBe(true);
      if (!runResult.ok) return;

      const taskId = runResult.value.id;
      // Manually set task as failed
      const task = mockTaskManager['taskStorage'].get(taskId);
      task.status = 'failed';

      const result = await simulateResumeCommand(mockTaskManager, taskId);

      expect(result.ok).toBe(true);
      expect(mockTaskManager.resumeCalls).toHaveLength(1);
      expect(mockTaskManager.resumeCalls[0].taskId).toBe(taskId);
    });

    it('should pass additional context to resume', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, 'original');
      expect(runResult.ok).toBe(true);
      if (!runResult.ok) return;

      const taskId = runResult.value.id;
      const task = mockTaskManager['taskStorage'].get(taskId);
      task.status = 'completed';

      const result = await simulateResumeCommand(mockTaskManager, taskId, 'Try a different approach');

      expect(result.ok).toBe(true);
      expect(mockTaskManager.resumeCalls[0].additionalContext).toBe('Try a different approach');
    });

    it('should return new task with retry metadata', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, 'original');
      expect(runResult.ok).toBe(true);
      if (!runResult.ok) return;

      const taskId = runResult.value.id;
      const task = mockTaskManager['taskStorage'].get(taskId);
      task.status = 'failed';

      const result = await simulateResumeCommand(mockTaskManager, taskId);

      expect(result.ok).toBe(true);
      if (result.ok) {
        expect(result.value.id).not.toBe(taskId);
        expect(result.value.retryCount).toBe(1);
        expect(result.value.parentTaskId).toBe(taskId);
      }
    });

    it('should reject resume for non-existent task', async () => {
      const result = await simulateResumeCommand(mockTaskManager, 'non-existent');

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.TASK_NOT_FOUND);
      }
    });

    it('should reject resume for non-terminal task', async () => {
      const runResult = await simulateRunCommand(mockTaskManager, 'original');
      expect(runResult.ok).toBe(true);
      if (!runResult.ok) return;

      const taskId = runResult.value.id;
      // Task is still in 'queued' status (non-terminal)

      const result = await simulateResumeCommand(mockTaskManager, taskId);

      expect(result.ok).toBe(false);
      if (!result.ok) {
        expect(result.error.code).toBe(ErrorCode.INVALID_OPERATION);
      }
    });
  });
});

describe('CLI - Help Text Coverage', () => {
  it('should include schedule commands in help text', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('schedule create');
    expect(helpText).toContain('schedule list');
    expect(helpText).toContain('schedule status');
    expect(helpText).toContain('schedule cancel');
    expect(helpText).toContain('schedule pause');
    expect(helpText).toContain('schedule resume');
  });

  it('should include pipeline command in help text', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('pipeline');
  });

  it('should include resume command in help text', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('resume');
    expect(helpText).toContain('--context');
  });

  it('should include short flag aliases in help text', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('--continue');
    expect(helpText).toContain('-c');
    expect(helpText).toContain('--deps');
    expect(helpText).toContain('-b');
    expect(helpText).toContain('--buffer');
  });

  it('should include scheduling examples', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('--cron');
  });

  it('should include --foreground flag in help text', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('--foreground');
    expect(helpText).toContain('-f');
  });

  it('should include list/ls and retry commands', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('list');
    expect(helpText).toContain('retry');
  });

  it('should include config subcommands', () => {
    const helpText = getHelpText();

    expect(helpText).toContain('config show');
    expect(helpText).toContain('config set');
    expect(helpText).toContain('config reset');
    expect(helpText).toContain('config path');
  });
});

// ============================================================================
// CLI - Global Flag Recognition
// ============================================================================

describe('CLI - Global Flags', () => {
  describe('--help and -h flags', () => {
    it('should recognize --help as a valid command (not unknown)', () => {
      // BUG FIX: Previously --help fell through to "Unknown command: --help" with exit 1
      // Now it should be treated like "help" command
      const recognizedCommands = ['help', '--help', '-h'];
      for (const cmd of recognizedCommands) {
        // Verify these are in the same routing branch as 'help'
        expect(cmd === 'help' || cmd === '--help' || cmd === '-h').toBe(true);
      }
    });

    it('should treat --help, -h, and help as equivalent commands', () => {
      // All three should route to showHelp() + exit 0, not "Unknown command" + exit 1
      const helpAliases = ['help', '--help', '-h'];
      // They should all be in the recognized set (not fall through to else)
      const unknownCommandPattern = /^--/;
      for (const alias of helpAliases) {
        const isRecognized = alias === 'help' || alias === '--help' || alias === '-h';
        expect(isRecognized).toBe(true);
      }
    });
  });

  describe('--version and -v flags', () => {
    it('should recognize --version as a valid command (not unknown)', () => {
      const versionAliases = ['--version', '-v'];
      for (const alias of versionAliases) {
        expect(alias === '--version' || alias === '-v').toBe(true);
      }
    });

    it('should not treat --version/-v as unknown commands', () => {
      // Previously any --flag as mainCommand fell through to "Unknown command"
      // Now --version and -v are explicitly handled
      const mainCommand = '--version';
      const isVersion = mainCommand === '--version' || mainCommand === '-v';
      expect(isVersion).toBe(true);
    });
  });
});

// ============================================================================
// CLI Lifecycle: waitForTaskCompletion, --detach, SIGINT
// ============================================================================

describe('CLI - Task Completion Lifecycle', () => {
  let eventBus: InMemoryEventBus;
  const mockLogger = {
    info: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
    debug: vi.fn(),
    child: vi.fn(() => mockLogger),
  };

  beforeEach(() => {
    eventBus = new InMemoryEventBus(loadConfiguration(), mockLogger);
  });

  afterEach(() => {
    eventBus.dispose();
  });

  describe('waitForTaskCompletion pattern', () => {
    it('should resolve with exit code 0 on TaskCompleted', async () => {
      const taskId = TaskId('task-test-1');
      const promise = waitForCompletion(eventBus, taskId);

      // Emit TaskCompleted for the target task
      await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
        taskId,
        exitCode: 0,
        duration: 1000,
      });

      const exitCode = await promise;
      expect(exitCode).toBe(0);
    });

    it('should resolve with non-zero exit code on TaskCompleted with failure', async () => {
      const taskId = TaskId('task-test-2');
      const promise = waitForCompletion(eventBus, taskId);

      await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
        taskId,
        exitCode: 42,
        duration: 500,
      });

      const exitCode = await promise;
      expect(exitCode).toBe(42);
    });

    it('should resolve with exit code from TaskFailed event', async () => {
      const taskId = TaskId('task-test-3');
      const promise = waitForCompletion(eventBus, taskId);

      await eventBus.emit<TaskFailedEvent>('TaskFailed', {
        taskId,
        error: new AutobeatError(ErrorCode.SYSTEM_ERROR, 'Process crashed'),
        exitCode: 137,
      });

      const exitCode = await promise;
      expect(exitCode).toBe(137);
    });

    it('should default to exit code 1 when TaskFailed has no exitCode', async () => {
      const taskId = TaskId('task-test-4');
      const promise = waitForCompletion(eventBus, taskId);

      await eventBus.emit<TaskFailedEvent>('TaskFailed', {
        taskId,
        error: new AutobeatError(ErrorCode.SYSTEM_ERROR, 'Unknown failure'),
      });

      const exitCode = await promise;
      expect(exitCode).toBe(1);
    });

    it('should resolve with exit code 1 on TaskCancelled', async () => {
      const taskId = TaskId('task-test-5');
      const promise = waitForCompletion(eventBus, taskId);

      await eventBus.emit<TaskCancelledEvent>('TaskCancelled', {
        taskId,
        reason: 'User requested',
      });

      const exitCode = await promise;
      expect(exitCode).toBe(1);
    });

    it('should resolve with exit code 1 on TaskTimeout', async () => {
      const taskId = TaskId('task-test-6');
      const promise = waitForCompletion(eventBus, taskId);

      await eventBus.emit<TaskTimeoutEvent>('TaskTimeout', {
        taskId,
        error: new AutobeatError(ErrorCode.TASK_TIMEOUT, 'Task exceeded timeout'),
      });

      const exitCode = await promise;
      expect(exitCode).toBe(1);
    });

    it('should ignore events for other task IDs', async () => {
      const targetTaskId = TaskId('task-target');
      const otherTaskId = TaskId('task-other');
      const promise = waitForCompletion(eventBus, targetTaskId);

      // Emit completion for a different task — should not resolve
      await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
        taskId: otherTaskId,
        exitCode: 0,
        duration: 100,
      });

      // Now emit for the target task
      await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
        taskId: targetTaskId,
        exitCode: 0,
        duration: 200,
      });

      const exitCode = await promise;
      expect(exitCode).toBe(0);
    });

    it('should stream OutputCaptured to stdout/stderr', async () => {
      const taskId = TaskId('task-output-test');
      const captured: Array<{ type: string; data: string }> = [];

      // Track writes
      const origStdoutWrite = process.stdout.write;
      const origStderrWrite = process.stderr.write;
      process.stdout.write = ((data: string) => {
        captured.push({ type: 'stdout', data });
        return true;
      }) as typeof process.stdout.write;
      process.stderr.write = ((data: string) => {
        captured.push({ type: 'stderr', data });
        return true;
      }) as typeof process.stderr.write;

      try {
        const promise = waitForCompletion(eventBus, taskId);

        await eventBus.emit<OutputCapturedEvent>('OutputCaptured', {
          taskId,
          outputType: 'stdout',
          data: 'hello world\n',
        });

        await eventBus.emit<OutputCapturedEvent>('OutputCaptured', {
          taskId,
          outputType: 'stderr',
          data: 'warning: something\n',
        });

        // Complete the task to resolve the promise
        await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
          taskId,
          exitCode: 0,
          duration: 300,
        });

        await promise;

        expect(captured).toContainEqual({ type: 'stdout', data: 'hello world\n' });
        expect(captured).toContainEqual({ type: 'stderr', data: 'warning: something\n' });
      } finally {
        process.stdout.write = origStdoutWrite;
        process.stderr.write = origStderrWrite;
      }
    });

    it('should only resolve once even if multiple terminal events fire', async () => {
      const taskId = TaskId('task-double-resolve');
      const promise = waitForCompletion(eventBus, taskId);

      // Emit both completed and failed — only the first should matter
      await eventBus.emit<TaskCompletedEvent>('TaskCompleted', {
        taskId,
        exitCode: 0,
        duration: 100,
      });
      await eventBus.emit<TaskFailedEvent>('TaskFailed', {
        taskId,
        error: new AutobeatError(ErrorCode.SYSTEM_ERROR, 'late failure'),
        exitCode: 1,
      });

      const exitCode = await promise;
      expect(exitCode).toBe(0); // First event wins
    });
  });

  describe('--foreground flag', () => {
    it('should be recognized as a valid run option', () => {
      const options = parseRunArgs(['--foreground', 'analyze codebase']);
      expect(options.foreground).toBe(true);
      expect(options.prompt).toBe('analyze codebase');
    });

    it('should support short form -f', () => {
      const options = parseRunArgs(['-f', 'analyze codebase']);
      expect(options.foreground).toBe(true);
    });

    it('should default foreground to undefined when not specified', () => {
      const options = parseRunArgs(['analyze codebase']);
      expect(options.foreground).toBeUndefined();
    });

    it('should combine with other flags', () => {
      const options = parseRunArgs(['--foreground', '--priority', 'P0', 'run tests']);
      expect(options.foreground).toBe(true);
      expect(options.priority).toBe('P0');
      expect(options.prompt).toBe('run tests');
    });
  });

  describe('Foreground mode - arg filtering', () => {
    it('should filter --foreground from args', () => {
      const args = ['--foreground', 'analyze', '--priority', 'P0'];
      const filtered = args.filter((arg) => arg !== '--foreground' && arg !== '-f');
      expect(filtered).toEqual(['analyze', '--priority', 'P0']);
      expect(filtered).not.toContain('--foreground');
    });

    it('should filter -f from args', () => {
      const args = ['-f', 'analyze', '--priority', 'P0'];
      const filtered = args.filter((arg) => arg !== '--foreground' && arg !== '-f');
      expect(filtered).toEqual(['analyze', '--priority', 'P0']);
      expect(filtered).not.toContain('-f');
    });

    it('should preserve other flags when filtering', () => {
      const args = ['--foreground', 'run tests', '-p', 'P0', '-w', '/workspace'];
      const filtered = args.filter((arg) => arg !== '--foreground' && arg !== '-f');
      expect(filtered).toEqual(['run tests', '-p', 'P0', '-w', '/workspace']);
      expect(filtered).toHaveLength(5);
    });

    it('should filter multiple --foreground and -f occurrences', () => {
      const args = ['--foreground', '-f', 'analyze', '--foreground'];
      const filtered = args.filter((arg) => arg !== '--foreground' && arg !== '-f');
      expect(filtered).toEqual(['analyze']);
    });
  });

  describe('Detach mode - task ID extraction', () => {
    it('should extract task ID from typical log output', () => {
      const logContent = [
        '🚀 Bootstrapping Autobeat...',
        '📝 Delegating task: analyze codebase',
        '✅ Task delegated successfully!',
        '📋 Task ID: task-abc123def456',
        '⏳ Waiting for task completion...',
      ].join('\n');

      const taskIdPattern = /Task ID:\s+(task-\S+)/;
      const match = logContent.match(taskIdPattern);
      expect(match).not.toBeNull();
      expect(match![1]).toBe('task-abc123def456');
    });

    it('should detect bootstrap failure pattern', () => {
      const logContent = '❌ Bootstrap failed: Database initialization error';
      const errorPattern = /^❌/m;
      expect(errorPattern.test(logContent)).toBe(true);
    });

    it('should not match task ID in non-matching output', () => {
      const logContent = '🚀 Bootstrapping Autobeat...\nStill loading...';
      const taskIdPattern = /Task ID:\s+(task-\S+)/;
      expect(logContent.match(taskIdPattern)).toBeNull();
    });

    it('should generate unique log file names', () => {
      const names = new Set<string>();
      for (let i = 0; i < 10; i++) {
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        const suffix = Math.random().toString(36).substring(2, 8);
        names.add(`detach-${timestamp}-${suffix}.log`);
      }
      // All names should be unique (random suffix ensures this)
      expect(names.size).toBe(10);
    });
  });

  describe('Detach mode - child args construction', () => {
    it('should construct child args with --foreground for background process', () => {
      const runArgs = ['analyze', '--priority', 'P0'];
      const childArgs = ['path/to/cli.js', 'run', '--foreground', ...runArgs];

      expect(childArgs).toEqual(['path/to/cli.js', 'run', '--foreground', 'analyze', '--priority', 'P0']);
      expect(childArgs).toContain('--foreground');
    });

    it('should preserve all flags in child args', () => {
      const runArgs = ['run tests', '-p', 'P0', '-w', '/workspace', '-t', '60000'];
      const childArgs = ['run', '--foreground', ...runArgs];

      expect(childArgs).toContain('--foreground');
      expect(childArgs).toContain('-p');
      expect(childArgs).toContain('P0');
      expect(childArgs).toContain('-w');
      expect(childArgs).toContain('/workspace');
      expect(childArgs).toContain('-t');
      expect(childArgs).toContain('60000');
    });

    it('should detect missing prompt in args', () => {
      const runArgs: string[] = [];
      const hasPrompt = runArgs.some((arg) => !arg.startsWith('-'));
      expect(hasPrompt).toBe(false);
    });

    it('should detect prompt present in args', () => {
      const runArgs = ['analyze code', '-p', 'P0'];
      const hasPrompt = runArgs.some((arg) => !arg.startsWith('-'));
      expect(hasPrompt).toBe(true);
    });
  });

  describe('SIGINT handling', () => {
    it('should cancel task when SIGINT is received', async () => {
      const mockTaskManager = new MockTaskManager();
      const runResult = await simulateRunCommand(mockTaskManager, 'long running task');
      expect(runResult.ok).toBe(true);
      if (!runResult.ok) return;

      const taskId = runResult.value.id;

      // Simulate SIGINT cancel flow
      await mockTaskManager.cancel(taskId, 'User interrupted (SIGINT)');

      expect(mockTaskManager.cancelCalls).toHaveLength(1);
      expect(mockTaskManager.cancelCalls[0].taskId).toBe(taskId);
      expect(mockTaskManager.cancelCalls[0].reason).toBe('User interrupted (SIGINT)');
    });
  });

  describe('Agent flag parsing (v0.5.0)', () => {
    it('should accept valid --agent flag values', () => {
      for (const agent of ['claude', 'codex']) {
        expect(isAgentProvider(agent)).toBe(true);
      }
    });

    it('should reject invalid agent names', () => {
      expect(isAgentProvider('unknown-agent')).toBe(false);
      expect(isAgentProvider('')).toBe(false);
      expect(isAgentProvider('gpt4')).toBe(false);
    });

    it('should parse --agent flag from CLI args', () => {
      const args = ['analyze code', '--agent', 'codex', '-p', 'P0'];

      let agentValue: string | undefined;
      for (let i = 0; i < args.length; i++) {
        if (args[i] === '--agent' || args[i] === '-a') {
          const next = args[i + 1];
          if (next && !next.startsWith('-') && isAgentProvider(next)) {
            agentValue = next;
          }
        }
      }

      expect(agentValue).toBe('codex');
    });

    it('should parse -a shorthand flag', () => {
      const args = ['analyze code', '-a', 'codex'];

      let agentValue: string | undefined;
      for (let i = 0; i < args.length; i++) {
        if (args[i] === '--agent' || args[i] === '-a') {
          const next = args[i + 1];
          if (next && !next.startsWith('-') && isAgentProvider(next)) {
            agentValue = next;
          }
        }
      }

      expect(agentValue).toBe('codex');
    });
  });

  describe('Agent List command (v0.5.0)', () => {
    it('should export listAgents function', async () => {
      const agentsModule = await import('../../src/cli/commands/agents');
      expect(typeof agentsModule.listAgents).toBe('function');
    });

    it('should list all AGENT_PROVIDERS', () => {
      expect(AGENT_PROVIDERS).toEqual(['claude', 'codex']);
    });
  });

  describe('Agent in status display (v0.5.0)', () => {
    it('should default to claude when agent is undefined', () => {
      const agentDisplay = (agent?: string) => agent ?? 'claude';
      expect(agentDisplay(undefined)).toBe('claude');
      expect(agentDisplay('codex')).toBe('codex');
    });
  });

  describe('Agent auth commands (v0.5.0)', () => {
    it('should export checkAgents function', async () => {
      const agentsModule = await import('../../src/cli/commands/agents');
      expect(typeof agentsModule.checkAgents).toBe('function');
    });

    it('should export agent config functions', async () => {
      const agentsModule = await import('../../src/cli/commands/agents');
      expect(typeof agentsModule.agentsConfigSet).toBe('function');
      expect(typeof agentsModule.agentsConfigShow).toBe('function');
      expect(typeof agentsModule.agentsConfigReset).toBe('function');
    });
  });
});

// ============================================================================
// Helper Functions - Simulate CLI commands without actually running CLI
// ============================================================================

function getHelpText(): string {
  // Simulate help text extraction - must match actual showHelp() output
  return `
🤖 Autobeat - MCP Server for Task Delegation

Usage:
  beat <command> [options...]

MCP Server Commands:
  mcp start              Start the MCP server

Task Commands:
  run <prompt> [options]       Delegate a task (fire-and-forget by default)
    -f, --foreground           Stream output and wait for task completion
    -p, --priority P0|P1|P2    Task priority
    --deps TASK_IDS            Comma-separated task IDs (alias: --depends-on)
    -c, --continue TASK_ID     Continue from checkpoint (alias: --continue-from)
    -b, --buffer BYTES         Max output buffer (alias: --max-output-buffer)
  list, ls                     List all tasks
  status [task-id]             Get status of task(s)
  logs <task-id> [--tail N]    Get output logs
  cancel <task-id> [reason]    Cancel a running task
  retry <task-id>              Retry a failed or completed task
  resume <task-id> [--context "additional instructions"]

Schedule Commands:
  schedule create <prompt> [options]   Create a scheduled task
    --cron "0 9 * * 1-5"              Cron expression (implies --type cron)
    --at "2025-03-01T09:00:00Z"       ISO 8601 datetime (implies --type one_time)
    --type cron|one_time               Explicit type (optional if --cron or --at given)
  schedule list [--status active|paused|...] [--limit N]
  schedule status <schedule-id> [--history] [--history-limit N]
  schedule cancel <schedule-id> [reason]
  schedule pause <schedule-id>
  schedule resume <schedule-id>

Pipeline Commands:
  pipeline <prompt> [<prompt>]...   Create chained one-time schedules

Configuration:
  config show                Show current configuration
  config set <key> <value>   Set a config value
  config reset <key>         Remove a key from config file
  config path                Print config file location

Examples:
  beat run "analyze codebase" --foreground
  beat status abc123
  beat list
  beat schedule create "run tests" --cron "0 9 * * 1-5"
  beat pipeline "setup db" "run migrations" "seed data"
  beat resume <task-id> --context "Try a different approach"
  beat config set timeout 300000
`;
}

function getConfigText(): string {
  return `
📋 MCP Configuration for Autobeat

Add this to your MCP configuration file:

{
  "mcpServers": {
    "autobeat": {
      "command": "npx",
      "args": ["-y", "autobeat", "mcp", "start"]
    }
  }
}

Configuration file locations:
- Claude Code: .mcp.json (in project root)
- Claude Desktop (macOS): ~/Library/Application Support/Claude/claude_desktop_config.json
- Claude Desktop (Windows): %APPDATA%\\Claude\\claude_desktop_config.json

For global installation, use:
{
  "mcpServers": {
    "autobeat": {
      "command": "beat",
      "args": ["mcp", "start"]
    }
  }
}

For local development, use /path/to/autobeat/dist/index.js
`;
}

interface RunOptions {
  priority?: string;
  workingDirectory?: string;
  timeout?: number;
  maxOutputBuffer?: number;
  dependsOn?: string[];
  continueFrom?: string;
  foreground?: boolean;
}

function validateRunInput(prompt: string, options: RunOptions) {
  if (!prompt || prompt.trim().length === 0) {
    return err(new AutobeatError(ErrorCode.INVALID_INPUT, 'Prompt is required', { field: 'prompt' }));
  }

  if (options.priority && !['P0', 'P1', 'P2'].includes(options.priority)) {
    return err(
      new AutobeatError(ErrorCode.INVALID_INPUT, 'Priority must be P0, P1, or P2', {
        field: 'priority',
        value: options.priority,
      }),
    );
  }

  if (options.workingDirectory) {
    const path = options.workingDirectory;
    if (!path.startsWith('/')) {
      return err(new AutobeatError(ErrorCode.INVALID_DIRECTORY, 'Working directory must be absolute path', { path }));
    }
    if (path.includes('..')) {
      return err(new AutobeatError(ErrorCode.INVALID_DIRECTORY, 'Path traversal not allowed', { path }));
    }
  }

  if (options.timeout !== undefined) {
    if (typeof options.timeout !== 'number' || options.timeout <= 0 || !isFinite(options.timeout)) {
      return err(
        new AutobeatError(ErrorCode.INVALID_INPUT, 'Timeout must be positive number', {
          field: 'timeout',
          value: options.timeout,
        }),
      );
    }
  }

  if (options.maxOutputBuffer !== undefined) {
    const maxAllowed = 1024 * 1024 * 100; // 100MB
    if (options.maxOutputBuffer > maxAllowed) {
      return err(
        new AutobeatError(ErrorCode.INVALID_INPUT, `maxOutputBuffer exceeds limit of ${maxAllowed} bytes`, {
          field: 'maxOutputBuffer',
          value: options.maxOutputBuffer,
        }),
      );
    }
  }

  return ok(undefined);
}

async function simulateRunCommand(taskManager: MockTaskManager, prompt: string, options?: RunOptions) {
  const validation = validateRunInput(prompt, options || {});
  if (!validation.ok) {
    return validation;
  }

  const request = {
    prompt,
    priority: options?.priority || 'P2',
    workingDirectory: options?.workingDirectory || process.cwd(),
    timeout: options?.timeout || 300000,
    maxOutputBuffer: options?.maxOutputBuffer || 10485760,
    dependsOn: options?.dependsOn,
    continueFrom: options?.continueFrom,
  };

  return await taskManager.delegate(request);
}

/**
 * Simulates `beat status <task-id>` — mirrors production code in status.ts
 * which calls ctx.taskRepository.findById(TaskId(taskId))
 */
async function simulateStatusCommand(ctx: MockReadOnlyContext, taskId: string): Promise<Result<Task | null>> {
  return await ctx.taskRepository.findById(TaskId(taskId));
}

/**
 * Simulates `beat status` (no task ID) — mirrors production code in status.ts
 * which calls ctx.taskRepository.findAll()
 */
async function simulateStatusCommandAll(ctx: MockReadOnlyContext): Promise<Result<readonly Task[]>> {
  return await ctx.taskRepository.findAll();
}

/**
 * Simulates `beat logs <task-id> [--tail N]` — mirrors production code in logs.ts
 * which calls ctx.taskRepository.findById() then ctx.outputRepository.get()
 * with optional tail slicing on the result.
 */
async function simulateLogsCommand(
  ctx: MockReadOnlyContext,
  taskId: string,
  tail?: number,
): Promise<Result<TaskOutput | null>> {
  // Step 1: Validate task exists (mirrors logs.ts line 14)
  const taskResult = await ctx.taskRepository.findById(TaskId(taskId));
  if (!taskResult.ok) return taskResult as Result<null>;
  if (!taskResult.value) return ok(null);

  // Step 2: Read output (mirrors logs.ts line 27)
  const outputResult = await ctx.outputRepository.get(TaskId(taskId));
  if (!outputResult.ok) return outputResult;
  if (!outputResult.value) return ok(null);

  const output = outputResult.value;

  // Step 3: Apply tail slicing (mirrors logs.ts lines 42-48)
  if (tail && tail > 0) {
    return ok({
      ...output,
      stdout: output.stdout.slice(-tail),
      stderr: output.stderr.slice(-tail),
    });
  }

  return ok(output);
}

/**
 * Simulates `beat schedule list [--status X]` — mirrors production code in schedule.ts
 * which calls ctx.scheduleRepository.findAll() or ctx.scheduleRepository.findByStatus()
 */
async function simulateScheduleListCommand(
  ctx: MockReadOnlyContext,
  status?: ScheduleStatus,
  limit?: number,
): Promise<Result<readonly Schedule[]>> {
  if (status) {
    return await ctx.scheduleRepository.findByStatus(status, limit);
  }
  return await ctx.scheduleRepository.findAll(limit);
}

/**
 * Simulates `beat schedule status <schedule-id>` — mirrors production code in schedule.ts
 * which calls ctx.scheduleRepository.findById()
 */
async function simulateScheduleStatusCommand(
  ctx: MockReadOnlyContext,
  scheduleId: string,
): Promise<Result<Schedule | null>> {
  return await ctx.scheduleRepository.findById(ScheduleId(scheduleId));
}

async function simulateCancelCommand(taskManager: MockTaskManager, taskId: string, reason?: string) {
  return await taskManager.cancel(taskId, reason);
}

async function simulateRetryCommand(taskManager: MockTaskManager, taskId: string) {
  return await taskManager.retry(taskId);
}

// ============================================================================
// Schedule, Pipeline, Resume Helpers
// ============================================================================

/**
 * Build CLI arg array from structured options for schedule create.
 */
function buildScheduleCreateArgs(options: {
  prompt: string;
  type: string;
  cron?: string;
  at?: string;
  timezone?: string;
  missedRunPolicy?: string;
  priority?: string;
  workingDirectory?: string;
  maxRuns?: number;
  expiresAt?: string;
  afterScheduleId?: string;
  agent?: string;
}): string[] {
  const args = options.prompt.split(' ');
  if (options.cron) args.push('--cron', options.cron);
  if (options.at) args.push('--at', options.at);
  if (!options.cron && !options.at) args.push('--type', options.type);
  if (options.timezone) args.push('--timezone', options.timezone);
  if (options.missedRunPolicy) args.push('--missed-run-policy', options.missedRunPolicy);
  if (options.priority) args.push('--priority', options.priority);
  if (options.workingDirectory) args.push('--working-directory', options.workingDirectory);
  if (options.maxRuns) args.push('--max-runs', String(options.maxRuns));
  if (options.expiresAt) args.push('--expires-at', options.expiresAt);
  if (options.afterScheduleId) args.push('--after', options.afterScheduleId);
  if (options.agent) args.push('--agent', options.agent);
  return args;
}

async function simulateScheduleCreate(
  service: MockScheduleService,
  options: {
    prompt: string;
    type: string;
    cron?: string;
    at?: string;
    timezone?: string;
    missedRunPolicy?: string;
    priority?: string;
    workingDirectory?: string;
    maxRuns?: number;
    expiresAt?: string;
    afterScheduleId?: string;
    agent?: string;
  },
) {
  const { parseScheduleCreateArgs } = await import('../../src/cli/commands/schedule');
  const parsed = parseScheduleCreateArgs(buildScheduleCreateArgs(options));
  if (!parsed.ok) return err(new AutobeatError(ErrorCode.INVALID_INPUT, parsed.error));
  const args = parsed.value;
  if (args.isPipeline) return err(new AutobeatError(ErrorCode.INVALID_INPUT, 'Expected non-pipeline'));

  return service.createSchedule({
    prompt: args.prompt,
    scheduleType: args.scheduleType === 'cron' ? ScheduleType.CRON : ScheduleType.ONE_TIME,
    cronExpression: args.cronExpression,
    scheduledAt: args.scheduledAt,
    timezone: args.timezone,
    missedRunPolicy: args.missedRunPolicy ? toMissedRunPolicy(args.missedRunPolicy) : undefined,
    priority: args.priority,
    workingDirectory: args.workingDirectory,
    maxRuns: args.maxRuns,
    expiresAt: args.expiresAt,
    afterScheduleId: args.afterScheduleId ? ScheduleId(args.afterScheduleId) : undefined,
  });
}

function validatePipelineInput(steps: string[]) {
  if (steps.length === 0) {
    return err(new AutobeatError(ErrorCode.INVALID_INPUT, 'No pipeline steps found', { field: 'steps' }));
  }
  return ok(undefined);
}

/**
 * Simulates `beat schedule create --pipeline --step "..." --step "..." --cron "..."`
 * Uses real parseScheduleCreateArgs for validation.
 */
async function simulateScheduleCreatePipeline(
  service: MockScheduleService,
  options: {
    steps: string[];
    cron?: string;
    at?: string;
    timezone?: string;
    missedRunPolicy?: string;
    priority?: string;
    workingDirectory?: string;
    maxRuns?: number;
    expiresAt?: string;
    afterScheduleId?: string;
    agent?: string;
  },
) {
  const { parseScheduleCreateArgs } = await import('../../src/cli/commands/schedule');
  const args: string[] = ['--pipeline'];
  for (const step of options.steps) {
    args.push('--step', step);
  }
  if (options.cron) args.push('--cron', options.cron);
  if (options.at) args.push('--at', options.at);
  if (options.timezone) args.push('--timezone', options.timezone);
  if (options.missedRunPolicy) args.push('--missed-run-policy', options.missedRunPolicy);
  if (options.priority) args.push('--priority', options.priority);
  if (options.workingDirectory) args.push('--working-directory', options.workingDirectory);
  if (options.maxRuns) args.push('--max-runs', String(options.maxRuns));
  if (options.expiresAt) args.push('--expires-at', options.expiresAt);
  if (options.afterScheduleId) args.push('--after', options.afterScheduleId);
  if (options.agent) args.push('--agent', options.agent);

  const parsed = parseScheduleCreateArgs(args);
  if (!parsed.ok) return err(new AutobeatError(ErrorCode.INVALID_INPUT, parsed.error));
  const p = parsed.value;
  if (!p.isPipeline) return err(new AutobeatError(ErrorCode.INVALID_INPUT, 'Expected pipeline'));

  return service.createScheduledPipeline({
    steps: p.pipelineSteps.map((prompt) => ({ prompt })),
    scheduleType: p.scheduleType === 'cron' ? ScheduleType.CRON : ScheduleType.ONE_TIME,
    cronExpression: p.cronExpression,
    scheduledAt: p.scheduledAt,
    timezone: p.timezone,
    missedRunPolicy: p.missedRunPolicy ? toMissedRunPolicy(p.missedRunPolicy) : undefined,
    priority: p.priority,
    workingDirectory: p.workingDirectory,
    maxRuns: p.maxRuns,
    expiresAt: p.expiresAt,
    afterScheduleId: p.afterScheduleId ? ScheduleId(p.afterScheduleId) : undefined,
    agent: p.agent,
  });
}

/**
 * Simulates `beat schedule cancel <id> [--cancel-tasks] [reason]`
 * Mirrors the scheduleCancel() function.
 */
async function simulateScheduleCancel(
  service: MockScheduleService,
  options: {
    scheduleId: string;
    reason?: string;
    cancelTasks?: boolean;
  },
) {
  return service.cancelSchedule(ScheduleId(options.scheduleId), options.reason, options.cancelTasks);
}

async function simulatePipeline(service: MockScheduleService, pipelineArgs: string[]) {
  // Each arg is a pipeline step prompt
  const validation = validatePipelineInput(pipelineArgs);
  if (!validation.ok) return validation;

  const scheduledAt = new Date(Date.now() + 2000).toISOString();
  let previousScheduleId: string | undefined;

  for (const prompt of pipelineArgs) {
    const result = await service.createSchedule({
      prompt,
      scheduleType: ScheduleType.ONE_TIME,
      scheduledAt,
      afterScheduleId: previousScheduleId ? ScheduleId(previousScheduleId) : undefined,
    });

    if (!result.ok) return result;
    previousScheduleId = result.value.id;
  }

  return ok(undefined);
}

async function simulateResumeCommand(taskManager: MockTaskManager, taskId: string, additionalContext?: string) {
  return taskManager.resume({
    taskId: TaskId(taskId),
    additionalContext,
  });
}

// ============================================================================
// Loop Command Helpers
// ============================================================================

async function simulateLoopCreate(service: MockLoopService, args: string[]) {
  const { parseLoopCreateArgs } = await import('../../src/cli/commands/loop');
  const parsed = parseLoopCreateArgs(args);
  if (!parsed.ok) return err(new AutobeatError(ErrorCode.INVALID_INPUT, parsed.error));
  const p = parsed.value;
  return service.createLoop({
    prompt: p.isPipeline ? undefined : p.prompt,
    strategy: p.strategy,
    exitCondition: p.exitCondition,
    evalDirection: toOptimizeDirection(p.evalDirection),
    evalTimeout: p.evalTimeout,
    workingDirectory: p.workingDirectory,
    maxIterations: p.maxIterations,
    maxConsecutiveFailures: p.maxConsecutiveFailures,
    cooldownMs: p.cooldownMs,
    freshContext: p.freshContext,
    pipelineSteps: p.isPipeline ? p.pipelineSteps : undefined,
    priority: p.priority ? Priority[p.priority] : undefined,
    agent: p.agent,
  });
}

async function simulateLoopListCommand(
  ctx: MockReadOnlyContext,
  status?: LoopStatus,
  limit?: number,
): Promise<Result<readonly Loop[]>> {
  if (status) {
    return await ctx.loopRepository.findByStatus(status, limit);
  }
  return await ctx.loopRepository.findAll(limit);
}

async function simulateLoopGetCommand(ctx: MockReadOnlyContext, loopId: string): Promise<Result<Loop | null>> {
  return await ctx.loopRepository.findById(LoopId(loopId));
}

async function simulateLoopCancel(
  service: MockLoopService,
  options: { loopId: string; reason?: string; cancelTasks?: boolean },
) {
  return service.cancelLoop(LoopId(options.loopId), options.reason, options.cancelTasks);
}

async function simulateLoopPause(service: MockLoopService, options: { loopId: string; force?: boolean }) {
  return service.pauseLoop(LoopId(options.loopId), { force: options.force });
}

async function simulateLoopResume(service: MockLoopService, options: { loopId: string }) {
  return service.resumeLoop(LoopId(options.loopId));
}

// ============================================================================
// Task Completion Lifecycle Helpers
// ============================================================================

/**
 * Mirrors waitForTaskCompletion() from cli.ts — subscribes to EventBus events
 * for a specific task and resolves on terminal state.
 *
 * This tests the exact same subscription pattern the production code uses,
 * validating the EventBus contract rather than importing the non-exportable function.
 */
function waitForCompletion(eventBus: InMemoryEventBus, taskId: string): Promise<number> {
  return new Promise((resolve) => {
    let resolved = false;
    const subscriptionIds: string[] = [];

    const cleanup = () => {
      for (const id of subscriptionIds) {
        eventBus.unsubscribe(id);
      }
    };

    const resolveOnce = (exitCode: number) => {
      if (resolved) return;
      resolved = true;
      cleanup();
      resolve(exitCode);
    };

    const outputSub = eventBus.subscribe<OutputCapturedEvent>('OutputCaptured', async (event) => {
      if (event.taskId !== taskId) return;
      const stream = event.outputType === 'stderr' ? process.stderr : process.stdout;
      stream.write(event.data);
    });
    if (outputSub.ok) subscriptionIds.push(outputSub.value);

    const completedSub = eventBus.subscribe<TaskCompletedEvent>('TaskCompleted', async (event) => {
      if (event.taskId !== taskId) return;
      resolveOnce(event.exitCode);
    });
    if (completedSub.ok) subscriptionIds.push(completedSub.value);

    const failedSub = eventBus.subscribe<TaskFailedEvent>('TaskFailed', async (event) => {
      if (event.taskId !== taskId) return;
      resolveOnce(event.exitCode ?? 1);
    });
    if (failedSub.ok) subscriptionIds.push(failedSub.value);

    const cancelledSub = eventBus.subscribe<TaskCancelledEvent>('TaskCancelled', async (event) => {
      if (event.taskId !== taskId) return;
      resolveOnce(1);
    });
    if (cancelledSub.ok) subscriptionIds.push(cancelledSub.value);

    const timeoutSub = eventBus.subscribe<TaskTimeoutEvent>('TaskTimeout', async (event) => {
      if (event.taskId !== taskId) return;
      resolveOnce(1);
    });
    if (timeoutSub.ok) subscriptionIds.push(timeoutSub.value);
  });
}

// ============================================================================
// Loop Command Tests
// ============================================================================

describe('CLI - Loop Commands', () => {
  // Dynamic import to avoid polluting the module cache for cli-services.test.ts
  // (non-isolated mode shares module cache; loop.ts transitively imports ui.js)
  let parseLoopCreateArgs: typeof import('../../src/cli/commands/loop').parseLoopCreateArgs;

  beforeAll(async () => {
    const mod = await import('../../src/cli/commands/loop');
    parseLoopCreateArgs = mod.parseLoopCreateArgs;
  });

  describe('parseLoopCreateArgs - pure function', () => {
    it('should parse retry strategy with --until', () => {
      const result = parseLoopCreateArgs(['fix', 'tests', '--until', 'npm test']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.strategy).toBe(LoopStrategy.RETRY);
      expect(result.value.exitCondition).toBe('npm test');
      if (result.value.isPipeline) return;
      expect(result.value.prompt).toBe('fix tests');
    });

    it('should parse optimize strategy with --eval and --maximize', () => {
      const result = parseLoopCreateArgs(['optimize', '--eval', 'echo 42', '--maximize']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.strategy).toBe(LoopStrategy.OPTIMIZE);
      expect(result.value.exitCondition).toBe('echo 42');
      expect(result.value.evalDirection).toBe('maximize');
    });

    it('should parse pipeline mode with --pipeline and --step flags', () => {
      const result = parseLoopCreateArgs(['--pipeline', '--step', 'lint', '--step', 'test', '--until', 'true']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.isPipeline).toBe(true);
      if (!result.value.isPipeline) return;
      expect(result.value.pipelineSteps).toEqual(['lint', 'test']);
    });

    it('should parse --max-iterations', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--max-iterations', '5']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.maxIterations).toBe(5);
    });

    it('should parse --max-failures', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--max-failures', '3']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.maxConsecutiveFailures).toBe(3);
    });

    it('should parse --cooldown', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--cooldown', '1000']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.cooldownMs).toBe(1000);
    });

    it('should parse --eval-timeout', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--eval-timeout', '5000']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalTimeout).toBe(5000);
    });

    it('should parse --checkpoint as freshContext=false', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--checkpoint']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.freshContext).toBe(false);
    });

    it('should default freshContext to true', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.freshContext).toBe(true);
    });

    it('should parse --priority P0', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--priority', 'P0']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.priority).toBe('P0');
    });

    it('should parse --agent claude', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--agent', 'claude']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.agent).toBe('claude');
    });

    it('should parse --working-directory', () => {
      const cwd = process.cwd();
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--working-directory', cwd]);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.workingDirectory).toBe(cwd);
    });

    it('should parse --max-iterations 0 as unlimited', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--max-iterations', '0']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.maxIterations).toBe(0);
    });

    // Error cases
    it('should reject both --until and --eval', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--eval', 'echo 1']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Cannot specify both');
    });

    it('should reject neither --until nor --eval', () => {
      const result = parseLoopCreateArgs(['fix']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--until');
    });

    it('should reject --eval without --minimize or --maximize', () => {
      const result = parseLoopCreateArgs(['fix', '--eval', 'echo 42']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--minimize or --maximize');
    });

    it('should reject --minimize/--maximize without --eval', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--maximize']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('only valid with --eval');
    });

    it('should reject both --minimize and --maximize', () => {
      const result = parseLoopCreateArgs(['fix', '--eval', 'echo 42', '--minimize', '--maximize']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Cannot specify both --minimize and --maximize');
    });

    it('should reject --pipeline with fewer than 2 --step', () => {
      const result = parseLoopCreateArgs(['--pipeline', '--step', 'only one', '--until', 'true']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('at least 2');
    });

    it('should reject unknown flag', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--bogus']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Unknown flag');
    });

    it('should reject negative --max-iterations', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--max-iterations', '-1']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--max-iterations');
    });

    it('should reject --eval-timeout below 1000ms', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--eval-timeout', '500']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--eval-timeout');
    });

    it('should parse --minimize flag correctly', () => {
      const result = parseLoopCreateArgs(['fix', '--eval', 'echo 1', '--minimize']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalDirection).toBe('minimize');
    });

    it('should reject --step without --pipeline', () => {
      const result = parseLoopCreateArgs(['fix', '--step', 'lint', '--step', 'test', '--until', 'true']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--pipeline');
    });

    it('should reject missing prompt for non-pipeline mode', () => {
      const result = parseLoopCreateArgs(['--until', 'true']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Usage');
    });

    it('should reject invalid priority', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--priority', 'P9']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Priority');
    });

    it('should reject unknown agent', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--agent', 'skynet']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Unknown agent');
    });

    it('should parse --git-branch flag', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--git-branch', 'feat/loop-work']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.gitBranch).toBe('feat/loop-work');
    });

    it('should leave gitBranch undefined when not specified', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.gitBranch).toBeUndefined();
    });

    it('should parse --eval-mode agent with --strategy retry', () => {
      const result = parseLoopCreateArgs(['fix', 'code', '--eval-mode', 'agent', '--strategy', 'retry']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalMode).toBe('agent');
      expect(result.value.strategy).toBe(LoopStrategy.RETRY);
    });

    it('should parse --eval-mode agent with --strategy optimize and --maximize', () => {
      const result = parseLoopCreateArgs([
        'optimize',
        'perf',
        '--eval-mode',
        'agent',
        '--strategy',
        'optimize',
        '--maximize',
      ]);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalMode).toBe('agent');
      expect(result.value.strategy).toBe(LoopStrategy.OPTIMIZE);
      expect(result.value.evalDirection).toBe('maximize');
    });

    it('should parse --eval-prompt with --eval-mode agent', () => {
      const result = parseLoopCreateArgs([
        'review',
        '--eval-mode',
        'agent',
        '--strategy',
        'retry',
        '--eval-prompt',
        'Check for security issues',
      ]);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalMode).toBe('agent');
      expect(result.value.evalPrompt).toBe('Check for security issues');
    });

    it('should reject --eval-prompt without --eval-mode agent', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--eval-prompt', 'Review changes']);
      expect(result.ok).toBe(false);
    });

    it('should reject --until with --eval-mode agent', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent', '--strategy', 'retry', '--until', 'npm test']);
      expect(result.ok).toBe(false);
    });

    it('should reject --eval with --eval-mode agent', () => {
      const result = parseLoopCreateArgs([
        'fix',
        '--eval-mode',
        'agent',
        '--strategy',
        'optimize',
        '--eval',
        'echo 42',
      ]);
      expect(result.ok).toBe(false);
    });

    it('should reject --eval-mode agent without --strategy', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent']);
      expect(result.ok).toBe(false);
    });

    it('should not require exitCondition for agent mode', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent', '--strategy', 'retry']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.exitCondition).toBe('');
    });

    it('should allow --maximize with --eval-mode agent and --strategy optimize', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent', '--strategy', 'optimize', '--maximize']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalDirection).toBe('maximize');
      expect(result.value.strategy).toBe(LoopStrategy.OPTIMIZE);
      expect(result.value.evalMode).toBe('agent');
    });

    it('should allow --minimize with --eval-mode agent and --strategy optimize', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent', '--strategy', 'optimize', '--minimize']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.evalDirection).toBe('minimize');
    });

    it('should reject --minimize/--maximize with --eval-mode agent and --strategy retry', () => {
      const result = parseLoopCreateArgs(['fix', '--eval-mode', 'agent', '--strategy', 'retry', '--maximize']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--minimize/--maximize');
    });

    it('should reject both --minimize and --maximize with --eval-mode agent', () => {
      const result = parseLoopCreateArgs([
        'fix',
        '--eval-mode',
        'agent',
        '--strategy',
        'optimize',
        '--minimize',
        '--maximize',
      ]);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('Cannot specify both');
    });

    it('should parse --system-prompt flag', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--system-prompt', 'Always respond in JSON']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.systemPrompt).toBe('Always respond in JSON');
    });

    it('should parse --system-prompt with dash-prefixed value (e.g., multi-line text)', () => {
      // A system prompt value that happens to start with a dash should be accepted
      // because the parser uses next === undefined check (not startsWith('-'))
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--system-prompt', '--special format OK']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.systemPrompt).toBe('--special format OK');
    });

    it('should leave systemPrompt undefined when not specified', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true']);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value.systemPrompt).toBeUndefined();
    });

    it('should reject --system-prompt with no value', () => {
      const result = parseLoopCreateArgs(['fix', '--until', 'true', '--system-prompt']);
      expect(result.ok).toBe(false);
      if (result.ok) return;
      expect(result.error).toContain('--system-prompt');
    });
  });

  describe('loop create — service integration', () => {
    let mockLoopService: MockLoopService;

    beforeEach(() => {
      mockLoopService = new MockLoopService();
    });

    afterEach(() => {
      mockLoopService.reset();
    });

    it('should create retry loop with correct service args', async () => {
      const result = await simulateLoopCreate(mockLoopService, [
        'fix',
        'tests',
        '--until',
        'npm test',
        '--max-iterations',
        '5',
      ]);
      expect(result.ok).toBe(true);
      expect(mockLoopService.createCalls).toHaveLength(1);
      expect(mockLoopService.createCalls[0].strategy).toBe(LoopStrategy.RETRY);
      expect(mockLoopService.createCalls[0].exitCondition).toBe('npm test');
      expect(mockLoopService.createCalls[0].maxIterations).toBe(5);
    });

    it('should create optimize loop with direction', async () => {
      const result = await simulateLoopCreate(mockLoopService, ['optimize', 'perf', '--eval', 'echo 42', '--maximize']);
      expect(result.ok).toBe(true);
      expect(mockLoopService.createCalls[0].strategy).toBe(LoopStrategy.OPTIMIZE);
      expect(mockLoopService.createCalls[0].evalDirection).toBe(OptimizeDirection.MAXIMIZE);
    });

    it('should create pipeline loop with steps', async () => {
      const result = await simulateLoopCreate(mockLoopService, [
        '--pipeline',
        '--step',
        'lint',
        '--step',
        'test',
        '--until',
        'true',
      ]);
      expect(result.ok).toBe(true);
      expect(mockLoopService.createCalls[0].pipelineSteps).toEqual(['lint', 'test']);
      expect(mockLoopService.createCalls[0].prompt).toBeUndefined();
    });

    it('should reject invalid args before calling service', async () => {
      const result = await simulateLoopCreate(mockLoopService, ['fix', '--until', 'true', '--eval', 'echo 1']);
      expect(result.ok).toBe(false);
      expect(mockLoopService.createCalls).toHaveLength(0);
    });

    it('should pass all optional parameters through', async () => {
      const result = await simulateLoopCreate(mockLoopService, [
        'full',
        'options',
        '--until',
        'true',
        '--max-iterations',
        '10',
        '--max-failures',
        '5',
        '--cooldown',
        '1000',
        '--eval-timeout',
        '5000',
        '--checkpoint',
        '--priority',
        'P0',
        '--agent',
        'claude',
      ]);
      expect(result.ok).toBe(true);
      const call = mockLoopService.createCalls[0];
      expect(call.maxIterations).toBe(10);
      expect(call.maxConsecutiveFailures).toBe(5);
      expect(call.cooldownMs).toBe(1000);
      expect(call.evalTimeout).toBe(5000);
      expect(call.freshContext).toBe(false);
      expect(call.agent).toBe('claude');
    });
  });

  describe('loop list — read-only context', () => {
    let mockLoopReadOnlyCtx: MockReadOnlyContext;

    beforeEach(() => {
      mockLoopReadOnlyCtx = new MockReadOnlyContext();
    });

    afterEach(() => {
      mockLoopReadOnlyCtx.reset();
    });

    it('should list all loops when no filter', async () => {
      const loop = createLoop(
        {
          prompt: 'test',
          strategy: LoopStrategy.RETRY,
          exitCondition: 'true',
        },
        '/workspace',
      );
      mockLoopReadOnlyCtx.addLoop(loop);

      const result = await simulateLoopListCommand(mockLoopReadOnlyCtx);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value).toHaveLength(1);
      expect(result.value[0].id).toBe(loop.id);
    });

    it('should filter by status', async () => {
      const loop1 = createLoop({ prompt: 'a', strategy: LoopStrategy.RETRY, exitCondition: 'true' }, '/w');
      const loop2 = Object.freeze({
        ...createLoop({ prompt: 'b', strategy: LoopStrategy.RETRY, exitCondition: 'true' }, '/w'),
        status: LoopStatus.COMPLETED,
      });
      mockLoopReadOnlyCtx.addLoop(loop1);
      mockLoopReadOnlyCtx.addLoop(loop2);

      const result = await simulateLoopListCommand(mockLoopReadOnlyCtx, LoopStatus.RUNNING);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value).toHaveLength(1);
      expect(result.value[0].status).toBe(LoopStatus.RUNNING);
    });

    it('should return empty array when no loops found', async () => {
      const result = await simulateLoopListCommand(mockLoopReadOnlyCtx);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value).toHaveLength(0);
    });
  });

  describe('loop status — read-only context', () => {
    let mockLoopReadOnlyCtx: MockReadOnlyContext;

    beforeEach(() => {
      mockLoopReadOnlyCtx = new MockReadOnlyContext();
    });

    afterEach(() => {
      mockLoopReadOnlyCtx.reset();
    });

    it('should get loop by ID', async () => {
      const loop = createLoop({ prompt: 'test', strategy: LoopStrategy.RETRY, exitCondition: 'true' }, '/workspace');
      mockLoopReadOnlyCtx.addLoop(loop);

      const result = await simulateLoopGetCommand(mockLoopReadOnlyCtx, loop.id);
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value).toBeDefined();
      expect(result.value!.id).toBe(loop.id);
    });

    it('should return null for missing loop', async () => {
      const result = await simulateLoopGetCommand(mockLoopReadOnlyCtx, 'loop-nonexistent');
      expect(result.ok).toBe(true);
      if (!result.ok) return;
      expect(result.value).toBeNull();
    });
  });

  describe('loop cancel — service integration', () => {
    let mockLoopService: MockLoopService;

    beforeEach(() => {
      mockLoopService = new MockLoopService();
    });

    afterEach(() => {
      mockLoopService.reset();
    });

    it('should cancel loop with reason', async () => {
      // First create a loop so it exists
      const createResult = await mockLoopService.createLoop({
        prompt: 'test',
        strategy: LoopStrategy.RETRY,
        exitCondition: 'true',
      });
      if (!createResult.ok) return;
      const loopId = createResult.value.id;

      const result = await simulateLoopCancel(mockLoopService, { loopId, reason: 'done' });
      expect(result.ok).toBe(true);
      expect(mockLoopService.cancelCalls).toHaveLength(1);
      expect(mockLoopService.cancelCalls[0].reason).toBe('done');
    });

    it('should pass cancel-tasks flag', async () => {
      const createResult = await mockLoopService.createLoop({
        prompt: 'test',
        strategy: LoopStrategy.RETRY,
        exitCondition: 'true',
      });
      if (!createResult.ok) return;
      const loopId = createResult.value.id;

      const result = await simulateLoopCancel(mockLoopService, { loopId, cancelTasks: true, reason: 'cleanup' });
      expect(result.ok).toBe(true);
      expect(mockLoopService.cancelCalls[0].cancelTasks).toBe(true);
    });

    it('should error on non-existent loop', async () => {
      const result = await simulateLoopCancel(mockLoopService, { loopId: 'loop-nonexistent' });
      expect(result.ok).toBe(false);
    });
  });

  describe('loop pause — service integration', () => {
    let mockLoopService: MockLoopService;

    beforeEach(() => {
      mockLoopService = new MockLoopService();
    });

    afterEach(() => {
      mockLoopService.reset();
    });

    it('should pause a running loop', async () => {
      const createResult = await mockLoopService.createLoop({
        prompt: 'pauseable loop',
        strategy: LoopStrategy.RETRY,
        exitCondition: 'true',
      });
      if (!createResult.ok) return;
      const loopId = createResult.value.id;

      const result = await simulateLoopPause(mockLoopService, { loopId });
      expect(result.ok).toBe(true);
      expect(mockLoopService.pauseCalls).toHaveLength(1);
      expect(mockLoopService.pauseCalls[0].options?.force).toBeFalsy();
    });

    it('should pass force flag', async () => {
      const createResult = await mockLoopService.createLoop({
        prompt: 'force pause loop',
        strategy: LoopStrategy.RETRY,
        exitCondition: 'true',
      });
      if (!createResult.ok) return;
      const loopId = createResult.value.id;

      const result = await simulateLoopPause(mockLoopService, { loopId, force: true });
      expect(result.ok).toBe(true);
      expect(mockLoopService.pauseCalls[0].options?.force).toBe(true);
    });

    it('should error on non-existent loop', async () => {
      const result = await simulateLoopPause(mockLoopService, { loopId: 'loop-nonexistent' });
      expect(result.ok).toBe(false);
    });
  });

  describe('loop resume — service integration', () => {
    let mockLoopService: MockLoopService;

    beforeEach(() => {
      mockLoopService = new MockLoopService();
    });

    afterEach(() => {
      mockLoopService.reset();
    });

    it('should resume a loop', async () => {
      const createResult = await mockLoopService.createLoop({
        prompt: 'resumable loop',
        strategy: LoopStrategy.RETRY,
        exitCondition: 'true',
      });
      if (!createResult.ok) return;
      const loopId = createResult.value.id;

      const result = await simulateLoopResume(mockLoopService, { loopId });
      expect(result.ok).toBe(true);
      expect(mockLoopService.resumeCalls).toHaveLength(1);
    });

    it('should error on non-existent loop', async () => {
      const result = await simulateLoopResume(mockLoopService, { loopId: 'loop-nonexistent' });
      expect(result.ok).toBe(false);
    });
  });

  describe('loop deprecated subcommand hints', () => {
    let handleLoopCommand: typeof import('../../src/cli/commands/loop').handleLoopCommand;

    beforeAll(async () => {
      const mod = await import('../../src/cli/commands/loop');
      handleLoopCommand = mod.handleLoopCommand;
    });

    it("should print rename hint for deprecated 'get' subcommand", async () => {
      const ui = await import('../../src/cli/ui');
      const errorSpy = vi.spyOn(ui, 'error').mockImplementation(() => {});
      const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
        throw new Error('process.exit');
      });

      await expect(handleLoopCommand('get', ['loop-123'])).rejects.toThrow('process.exit');

      expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('beat loop status'));
      expect(exitSpy).toHaveBeenCalledWith(1);

      errorSpy.mockRestore();
      exitSpy.mockRestore();
    });
  });
});

describe('CLI - Schedule --loop flag', () => {
  let parseScheduleCreateArgs: typeof import('../../src/cli/commands/schedule').parseScheduleCreateArgs;

  beforeAll(async () => {
    const mod = await import('../../src/cli/commands/schedule');
    parseScheduleCreateArgs = mod.parseScheduleCreateArgs;
  });

  it('should parse --loop with --until for retry strategy', () => {
    const result = parseScheduleCreateArgs(['--loop', '--until', 'npm test', '--cron', '0 9 * * *', 'Fix the tests']);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.strategy).toBe(LoopStrategy.RETRY);
    expect(result.value.loopConfig.exitCondition).toBe('npm test');
    expect(result.value.loopConfig.prompt).toBe('Fix the tests');
    expect(result.value.cronExpression).toBe('0 9 * * *');
  });

  it('should parse --loop with --eval and --minimize for optimize strategy', () => {
    const result = parseScheduleCreateArgs(['--loop', '--eval', 'echo 42', '--minimize', '--cron', '0 */6 * * *']);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.strategy).toBe(LoopStrategy.OPTIMIZE);
    expect(result.value.loopConfig.evalDirection).toBe('minimize');
  });

  it('should parse --loop with --max-iterations and --max-failures', () => {
    const result = parseScheduleCreateArgs([
      '--loop',
      '--until',
      'true',
      '--max-iterations',
      '20',
      '--max-failures',
      '5',
      '--cron',
      '0 * * * *',
    ]);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.maxIterations).toBe(20);
    expect(result.value.loopConfig.maxConsecutiveFailures).toBe(5);
  });

  it('should parse --loop with --git-branch flag', () => {
    const result = parseScheduleCreateArgs([
      '--loop',
      '--until',
      'true',
      '--git-branch',
      'feat/nightly-loop',
      '--cron',
      '0 0 * * *',
    ]);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.gitBranch).toBe('feat/nightly-loop');
  });

  it('should parse --loop with --checkpoint', () => {
    const result = parseScheduleCreateArgs(['--loop', '--until', 'true', '--checkpoint', '--cron', '0 0 * * *']);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.freshContext).toBe(false);
  });

  it('should parse --loop --pipeline with --step flags', () => {
    const result = parseScheduleCreateArgs([
      '--loop',
      '--pipeline',
      '--step',
      'lint',
      '--step',
      'test',
      '--until',
      'true',
      '--cron',
      '0 9 * * *',
    ]);
    expect(result.ok).toBe(true);
    if (!result.ok) return;
    expect(result.value.isLoop).toBe(true);
    if (!result.value.isLoop) return;
    expect(result.value.loopConfig.pipelineSteps).toEqual(['lint', 'test']);
  });

  it('should reject --loop without --until or --eval', () => {
    const result = parseScheduleCreateArgs(['--loop', '--cron', '0 9 * * *']);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.error).toContain('--until');
  });

  it('should reject --loop --eval without --minimize or --maximize', () => {
    const result = parseScheduleCreateArgs(['--loop', '--eval', 'echo 42', '--cron', '0 9 * * *']);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.error).toContain('--minimize or --maximize');
  });

  it('should reject --loop with both --minimize and --maximize', () => {
    const result = parseScheduleCreateArgs([
      '--loop',
      '--eval',
      'echo 42',
      '--minimize',
      '--maximize',
      '--cron',
      '0 9 * * *',
    ]);
    expect(result.ok).toBe(false);
    if (result.ok) return;
    expect(result.error).toContain('Cannot specify both --minimize and --maximize');
  });
});

/**
 * Parse run command args — mirrors the option parsing loop in cli.ts
 * for testing flag recognition without running the full CLI.
 */
function parseRunArgs(args: string[]): RunOptions & { prompt: string } {
  const options: RunOptions & { prompt: string } = { prompt: '' };
  const promptWords: string[] = [];

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    const next = args[i + 1];

    if (arg === '--foreground' || arg === '-f') {
      (options as RunOptions & { foreground?: boolean }).foreground = true;
    } else if ((arg === '--priority' || arg === '-p') && next) {
      options.priority = next;
      i++;
    } else if ((arg === '--working-directory' || arg === '-w') && next) {
      options.workingDirectory = next;
      i++;
    } else if ((arg === '--depends-on' || arg === '--deps') && next) {
      options.dependsOn = next.split(',').map((id) => id.trim());
      i++;
    } else if ((arg === '--continue-from' || arg === '--continue' || arg === '-c') && next) {
      options.continueFrom = next;
      i++;
    } else if ((arg === '--timeout' || arg === '-t') && next) {
      options.timeout = parseInt(next);
      i++;
    } else if ((arg === '--max-output-buffer' || arg === '--buffer' || arg === '-b') && next) {
      options.maxOutputBuffer = parseInt(next);
      i++;
    } else if (!arg.startsWith('-')) {
      promptWords.push(arg);
    }
  }

  options.prompt = promptWords.join(' ');
  return options;
}
