import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import {
  resetStateForTests,
  setOriginalCwd,
  setProjectRoot,
} from '../../bootstrap/state'
import {
  createManagedAutonomyFlowKey,
  DEFAULT_AUTONOMY_OWNER_KEY,
  formatAutonomyFlowDetail,
  formatAutonomyFlowsList,
  formatAutonomyFlowsStatus,
  getAutonomyFlowById,
  listAutonomyFlows,
  markManagedAutonomyFlowStepCancelled,
  markManagedAutonomyFlowStepCompleted,
  markManagedAutonomyFlowStepFailed,
  markManagedAutonomyFlowStepRunning,
  queueManagedAutonomyFlowStepRun,
  requestManagedAutonomyFlowCancel,
  resolveAutonomyFlowsPath,
  resumeManagedAutonomyFlow,
  startManagedAutonomyFlow,
  type AutonomyFlowRecord,
  type ManagedAutonomyFlowStepDefinition,
} from '../autonomyFlows'
import { AUTONOMY_DIR } from '../autonomyAuthority'
import { cleanupTempDir, createTempDir } from '../../../tests/mocks/file-system'

let tempDir = ''

beforeEach(async () => {
  tempDir = await createTempDir('autonomy-flows-')
  resetStateForTests()
  setOriginalCwd(tempDir)
  setProjectRoot(tempDir)
})

afterEach(async () => {
  resetStateForTests()
  if (tempDir) {
    await cleanupTempDir(tempDir)
  }
})

const TWO_STEPS: ManagedAutonomyFlowStepDefinition[] = [
  { name: 'gather', prompt: 'Gather inputs' },
  { name: 'draft', prompt: 'Draft the report' },
]

const STEPS_WITH_WAIT: ManagedAutonomyFlowStepDefinition[] = [
  { name: 'gather', prompt: 'Gather inputs', waitFor: 'manual-review' },
  { name: 'draft', prompt: 'Draft the report' },
]

describe('createManagedAutonomyFlowKey', () => {
  test('builds deterministic key from trigger and sourceId', () => {
    const key = createManagedAutonomyFlowKey({
      trigger: 'scheduled-task',
      sourceId: 'cron-1',
      goal: 'Do stuff',
    })
    expect(key).toBe('managed:scheduled-task:cron-1')
  })

  test('uses sourceLabel when sourceId is absent', () => {
    const key = createManagedAutonomyFlowKey({
      trigger: 'scheduled-task',
      sourceLabel: 'nightly',
      goal: 'Do stuff',
    })
    expect(key).toBe('managed:scheduled-task:nightly')
  })

  test('falls back to goal when no sourceId or sourceLabel', () => {
    const key = createManagedAutonomyFlowKey({
      trigger: 'scheduled-task',
      goal: 'Do stuff',
    })
    expect(key).toBe('managed:scheduled-task:Do stuff')
  })

  test('uses heartbeat-loop for proactive-tick without sourceId', () => {
    const key = createManagedAutonomyFlowKey({
      trigger: 'proactive-tick',
      goal: 'Tick goal',
    })
    expect(key).toBe('managed:proactive-tick:heartbeat-loop')
  })
})

describe('resolveAutonomyFlowsPath', () => {
  test('resolves to flows.json under the autonomy dir', () => {
    const path = resolveAutonomyFlowsPath(tempDir)
    expect(path).toContain(AUTONOMY_DIR)
    expect(path).toContain('flows.json')
  })
})

describe('listAutonomyFlows', () => {
  test('returns empty array when no file exists', async () => {
    const flows = await listAutonomyFlows(tempDir)
    expect(flows).toEqual([])
  })

  test('reads and normalizes flow records from disk', async () => {
    const flowsPath = resolveAutonomyFlowsPath(tempDir)
    await mkdir(join(tempDir, AUTONOMY_DIR), { recursive: true })
    await writeFile(
      flowsPath,
      JSON.stringify({
        flows: [
          {
            flowId: 'flow-1',
            flowKey: 'managed:scheduled-task:cron-1',
            syncMode: 'managed',
            trigger: 'scheduled-task',
            status: 'queued',
            rootDir: tempDir,
            goal: 'Test goal',
            createdAt: 100,
            updatedAt: 200,
            revision: 1,
            runCount: 0,
            ownerKey: DEFAULT_AUTONOMY_OWNER_KEY,
            currentDir: tempDir,
            boundary: [
              ' src/utils/** ',
              '/absolute/not-allowed',
              'src\\windows',
              '../outside',
              'src/utils/**',
              'docs/*.md',
            ],
            stateJson: {
              currentStepIndex: 0,
              steps: [
                {
                  stepId: 'step-1',
                  name: 'gather',
                  prompt: 'Gather inputs',
                  status: 'pending',
                },
              ],
            },
          },
        ],
      }),
      'utf-8',
    )

    const flows = await listAutonomyFlows(tempDir)
    expect(flows).toHaveLength(1)
    expect(flows[0]?.flowId).toBe('flow-1')
    expect(flows[0]?.syncMode).toBe('managed')
    expect(flows[0]?.boundary).toEqual(['src/utils/**', 'docs/*.md'])
    expect(flows[0]?.stateJson?.steps).toHaveLength(1)
  })

  test('filters out invalid records', async () => {
    const flowsPath = resolveAutonomyFlowsPath(tempDir)
    await mkdir(join(tempDir, AUTONOMY_DIR), { recursive: true })
    await writeFile(
      flowsPath,
      JSON.stringify({
        flows: [
          {
            flowId: 'valid-flow',
            flowKey: 'k',
            trigger: 'scheduled-task',
            status: 'queued',
            rootDir: tempDir,
            createdAt: 1,
            updatedAt: 2,
            goal: 'g',
            revision: 0,
            runCount: 0,
            ownerKey: 'main-thread',
            currentDir: tempDir,
          },
          { bad: true },
          null,
        ],
      }),
      'utf-8',
    )

    const flows = await listAutonomyFlows(tempDir)
    expect(flows).toHaveLength(1)
    expect(flows[0]?.flowId).toBe('valid-flow')
  })

  test('returns empty array for malformed JSON', async () => {
    const flowsPath = resolveAutonomyFlowsPath(tempDir)
    await mkdir(join(tempDir, AUTONOMY_DIR), { recursive: true })
    await writeFile(flowsPath, 'not json', 'utf-8')

    const flows = await listAutonomyFlows(tempDir)
    expect(flows).toEqual([])
  })

  test('persistence pruning keeps active flows ahead of recent terminal history', async () => {
    const flows: AutonomyFlowRecord[] = [
      {
        flowId: 'old-active',
        flowKey: 'managed:scheduled-task:old-active',
        syncMode: 'managed',
        ownerKey: DEFAULT_AUTONOMY_OWNER_KEY,
        revision: 1,
        trigger: 'scheduled-task',
        status: 'queued',
        goal: 'old active',
        rootDir: tempDir,
        currentDir: tempDir,
        runCount: 0,
        createdAt: 1,
        updatedAt: 1,
      },
      ...Array.from({ length: 100 }, (_, index) => ({
        flowId: `history-${index}`,
        flowKey: `managed:scheduled-task:history-${index}`,
        syncMode: 'managed' as const,
        ownerKey: DEFAULT_AUTONOMY_OWNER_KEY,
        revision: 1,
        trigger: 'scheduled-task' as const,
        status: 'succeeded' as const,
        goal: `history ${index}`,
        rootDir: tempDir,
        currentDir: tempDir,
        runCount: 1,
        createdAt: 1_000 + index,
        updatedAt: 1_000 + index,
        endedAt: 2_000 + index,
      })),
    ]
    const flowsPath = resolveAutonomyFlowsPath(tempDir)
    await mkdir(join(tempDir, AUTONOMY_DIR), { recursive: true })
    await writeFile(
      flowsPath,
      `${JSON.stringify({ flows }, null, 2)}\n`,
      'utf-8',
    )

    await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'fresh active',
      steps: TWO_STEPS,
      rootDir: tempDir,
      currentDir: tempDir,
      sourceId: 'fresh-active',
      nowMs: 9_999,
    })

    const persisted = await listAutonomyFlows(tempDir)
    expect(persisted).toHaveLength(100)
    expect(persisted.some(flow => flow.flowId === 'old-active')).toBe(true)
    expect(persisted.some(flow => flow.flowId === 'history-0')).toBe(false)
  })
})

describe('startManagedAutonomyFlow', () => {
  test('returns null when steps array is empty', async () => {
    const result = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Test',
      steps: [],
      rootDir: tempDir,
    })
    expect(result).toBeNull()
  })

  test('creates a new flow with queued status and returns nextStep', async () => {
    const result = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Ship report',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })

    expect(result).not.toBeNull()
    expect(result!.started).toBe(true)
    expect(result!.flow.status).toBe('queued')
    expect(result!.flow.goal).toBe('Ship report')
    expect(result!.flow.currentStep).toBe('gather')
    expect(result!.flow.stateJson?.steps).toHaveLength(2)
    expect(result!.flow.stateJson?.steps[0]?.status).toBe('pending')
    expect(result!.nextStep).toBeDefined()
    expect(result!.nextStep!.stepIndex).toBe(0)
    expect(result!.nextStep!.step.name).toBe('gather')
  })

  test('normalizes and preserves boundary across completed flow restarts', async () => {
    const first = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Scoped flow',
      steps: [{ name: 'only', prompt: 'Do it' }],
      rootDir: tempDir,
      sourceId: 'scoped-src',
      boundary: [' src/utils/** ', 'src\\bad', '/absolute', 'docs/*.md'],
      nowMs: 1000,
    })
    const flowId = first!.flow.flowId

    expect(first!.flow.boundary).toEqual(['src/utils/**', 'docs/*.md'])

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: first!.nextStep!.step.stepId,
      stepIndex: 0,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 3000,
    })

    const restarted = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Scoped flow',
      steps: [{ name: 'only', prompt: 'Do it again' }],
      rootDir: tempDir,
      sourceId: 'scoped-src',
      nowMs: 4000,
    })

    expect(restarted!.started).toBe(true)
    expect(restarted!.flow.flowId).toBe(flowId)
    expect(restarted!.flow.boundary).toEqual(['src/utils/**', 'docs/*.md'])
  })

  test('sets status=waiting when first step has waitFor', async () => {
    const result = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Wait flow',
      steps: STEPS_WITH_WAIT,
      rootDir: tempDir,
      nowMs: 1000,
    })

    expect(result!.started).toBe(true)
    expect(result!.flow.status).toBe('waiting')
    expect(result!.flow.waitJson).toBeDefined()
    expect(result!.flow.waitJson!.reason).toBe('manual-review')
    expect(result!.nextStep).toBeUndefined()
  })

  test('returns started=false if active flow with same key exists', async () => {
    const first = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Ship report',
      steps: TWO_STEPS,
      rootDir: tempDir,
      sourceId: 'dup-key',
      nowMs: 1000,
    })
    expect(first!.started).toBe(true)

    const second = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Ship report',
      steps: TWO_STEPS,
      rootDir: tempDir,
      sourceId: 'dup-key',
      nowMs: 2000,
    })
    expect(second!.started).toBe(false)
    expect(second!.flow.flowId).toBe(first!.flow.flowId)
  })

  test('reuses flowId when restarting a completed flow', async () => {
    // Start and complete a flow
    const first = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Repeatable',
      steps: [{ name: 'only', prompt: 'Do it' }],
      rootDir: tempDir,
      sourceId: 'repeat-src',
      nowMs: 1000,
    })
    const flowId = first!.flow.flowId

    // Queue and complete
    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: first!.nextStep!.step.stepId,
      stepIndex: 0,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 3000,
    })
    await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 4000,
    })

    // Verify it completed
    const completed = await getAutonomyFlowById(flowId, tempDir)
    expect(completed!.status).toBe('succeeded')

    // Restart with the same key
    const restarted = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Repeatable',
      steps: [{ name: 'only', prompt: 'Do it again' }],
      rootDir: tempDir,
      sourceId: 'repeat-src',
      nowMs: 5000,
    })

    expect(restarted!.started).toBe(true)
    expect(restarted!.flow.flowId).toBe(flowId)
    expect(restarted!.flow.revision).toBeGreaterThan(first!.flow.revision)
  })

  test('persists the flow to disk', async () => {
    await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Persist test',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })

    const raw = await readFile(resolveAutonomyFlowsPath(tempDir), 'utf-8')
    const parsed = JSON.parse(raw) as { flows: AutonomyFlowRecord[] }
    expect(parsed.flows).toHaveLength(1)
    expect(parsed.flows[0]?.goal).toBe('Persist test')
  })
})

describe('full lifecycle: start → queue → running → completed → succeeded', () => {
  test('advances through all steps to succeeded', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Full lifecycle',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    // Queue step 0
    const queued = await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    expect(queued!.status).toBe('queued')
    expect(queued!.latestRunId).toBe('run-0')
    expect(queued!.runCount).toBe(1)

    // Mark step 0 running
    const running = await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })
    expect(running!.status).toBe('running')
    expect(running!.startedAt).toBe(3000)

    // Complete step 0 — should auto-advance to step 1
    const advanced = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 4000,
    })
    expect(advanced!.flow.status).toBe('queued')
    expect(advanced!.flow.currentStep).toBe('draft')
    expect(advanced!.nextStep).toBeDefined()
    expect(advanced!.nextStep!.stepIndex).toBe(1)
    const step1Id = advanced!.nextStep!.step.stepId

    // Queue step 1
    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step1Id,
      stepIndex: 1,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 5000,
    })

    // Mark step 1 running
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 6000,
    })

    // Complete step 1 — no more steps, should succeed
    const final = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 7000,
    })
    expect(final!.flow.status).toBe('succeeded')
    expect(final!.flow.endedAt).toBe(7000)
    expect(final!.nextStep).toBeUndefined()

    // Verify persisted state
    const persisted = await getAutonomyFlowById(flowId, tempDir)
    expect(persisted!.status).toBe('succeeded')
    expect(persisted!.stateJson?.steps[0]?.status).toBe('completed')
    expect(persisted!.stateJson?.steps[1]?.status).toBe('completed')
  })
})

describe('lifecycle: step failure', () => {
  test('marks flow as failed when step fails', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Fail lifecycle',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })

    const failed = await markManagedAutonomyFlowStepFailed({
      flowId,
      runId: 'run-0',
      error: 'Something broke',
      rootDir: tempDir,
      nowMs: 4000,
    })

    expect(failed!.flow.status).toBe('failed')
    expect(failed!.flow.lastError).toBe('Something broke')
    expect(failed!.flow.blockedRunId).toBe('run-0')
    expect(failed!.flow.endedAt).toBe(4000)
    expect(failed!.flow.stateJson?.steps[0]?.status).toBe('failed')
    expect(failed!.flow.stateJson?.steps[0]?.error).toBe('Something broke')
  })
})

describe('lifecycle: waitFor → resume', () => {
  test('starts waiting then resumes and completes', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Wait then resume',
      steps: STEPS_WITH_WAIT,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    expect(startResult!.flow.status).toBe('waiting')
    expect(startResult!.nextStep).toBeUndefined()

    // Resume the waiting flow
    const resumed = await resumeManagedAutonomyFlow({
      flowId,
      rootDir: tempDir,
      nowMs: 2000,
    })
    expect(resumed!.flow.status).toBe('queued')
    expect(resumed!.flow.waitJson).toBeUndefined()
    expect(resumed!.nextStep).toBeDefined()
    expect(resumed!.nextStep!.step.name).toBe('gather')

    // Queue, run, complete step 0
    const step0Id = resumed!.nextStep!.step.stepId
    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 4000,
    })
    const afterStep0 = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 5000,
    })

    // Step 1 has no waitFor, so should auto-queue
    expect(afterStep0!.flow.status).toBe('queued')
    expect(afterStep0!.nextStep!.step.name).toBe('draft')

    // Complete step 1
    const step1Id = afterStep0!.nextStep!.step.stepId
    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step1Id,
      stepIndex: 1,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 6000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 7000,
    })
    const final = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-1',
      rootDir: tempDir,
      nowMs: 8000,
    })
    expect(final!.flow.status).toBe('succeeded')
  })
})

describe('lifecycle: next step has waitFor', () => {
  test('completing a step transitions to waiting when next step has waitFor', async () => {
    const steps: ManagedAutonomyFlowStepDefinition[] = [
      { name: 'step-a', prompt: 'Do A' },
      { name: 'step-b', prompt: 'Do B', waitFor: 'approval' },
    ]
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Wait mid-flow',
      steps,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })
    const afterStep0 = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 4000,
    })

    expect(afterStep0!.flow.status).toBe('waiting')
    expect(afterStep0!.flow.waitJson).toBeDefined()
    expect(afterStep0!.flow.waitJson!.reason).toBe('approval')
    expect(afterStep0!.flow.waitJson!.stepName).toBe('step-b')
    expect(afterStep0!.nextStep).toBeUndefined()
  })
})

describe('requestManagedAutonomyFlowCancel', () => {
  test('immediate cancel when not running (queued)', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Cancel test',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId

    const cancelResult = await requestManagedAutonomyFlowCancel({
      flowId,
      rootDir: tempDir,
      nowMs: 2000,
    })

    expect(cancelResult!.accepted).toBe(true)
    expect(cancelResult!.flow.status).toBe('cancelled')
    expect(cancelResult!.flow.endedAt).toBe(2000)
  })

  test('deferred cancel when step is running, completes on next step completion', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Deferred cancel',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    // Queue and start running
    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })

    // Request cancel while running — should be deferred
    const cancelResult = await requestManagedAutonomyFlowCancel({
      flowId,
      rootDir: tempDir,
      nowMs: 4000,
    })
    expect(cancelResult!.accepted).toBe(true)
    expect(cancelResult!.flow.status).toBe('running') // Still running
    expect(cancelResult!.flow.cancelRequestedAt).toBe(4000)

    // Complete the step — cancel should now take effect
    const completed = await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 5000,
    })
    expect(completed!.flow.status).toBe('cancelled')
    expect(completed!.flow.endedAt).toBe(5000)
    // Remaining steps should be cancelled
    expect(completed!.flow.stateJson?.steps[1]?.status).toBe('cancelled')
  })

  test('returns accepted=false for already completed flow', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Already done',
      steps: [{ name: 'only', prompt: 'Do it' }],
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const stepId = startResult!.nextStep!.step.stepId

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })
    await markManagedAutonomyFlowStepCompleted({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 4000,
    })

    const cancelResult = await requestManagedAutonomyFlowCancel({
      flowId,
      rootDir: tempDir,
      nowMs: 5000,
    })
    expect(cancelResult!.accepted).toBe(false)
  })

  test('returns null for unknown flowId', async () => {
    const cancelResult = await requestManagedAutonomyFlowCancel({
      flowId: 'nonexistent',
      rootDir: tempDir,
      nowMs: 1000,
    })
    expect(cancelResult).toBeNull()
  })
})

describe('markManagedAutonomyFlowStepCancelled', () => {
  test('cancels the step and all remaining steps', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Cancel step',
      steps: [
        { name: 's1', prompt: 'p1' },
        { name: 's2', prompt: 'p2' },
        { name: 's3', prompt: 'p3' },
      ],
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })

    const cancelled = await markManagedAutonomyFlowStepCancelled({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })

    expect(cancelled!.flow.status).toBe('cancelled')
    expect(cancelled!.flow.endedAt).toBe(3000)
    expect(cancelled!.flow.stateJson?.steps[0]?.status).toBe('cancelled')
    expect(cancelled!.flow.stateJson?.steps[1]?.status).toBe('cancelled')
    expect(cancelled!.flow.stateJson?.steps[2]?.status).toBe('cancelled')
  })
})

describe('resumeManagedAutonomyFlow', () => {
  test('returns unchanged flow when not in waiting status', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Not waiting',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId

    const resumed = await resumeManagedAutonomyFlow({
      flowId,
      rootDir: tempDir,
      nowMs: 2000,
    })

    // Flow is queued, not waiting, so resume should not change status
    expect(resumed!.flow.status).toBe('queued')
  })

  test('cancels when cancel was requested during wait', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Cancel during wait',
      steps: STEPS_WITH_WAIT,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    expect(startResult!.flow.status).toBe('waiting')

    // Request cancel while waiting
    await requestManagedAutonomyFlowCancel({
      flowId,
      rootDir: tempDir,
      nowMs: 2000,
    })

    // The flow should already be cancelled since it's not running
    const flow = await getAutonomyFlowById(flowId, tempDir)
    expect(flow!.status).toBe('cancelled')
  })
})

describe('getAutonomyFlowById', () => {
  test('returns null when flow does not exist', async () => {
    const flow = await getAutonomyFlowById('nonexistent', tempDir)
    expect(flow).toBeNull()
  })

  test('returns the flow when it exists', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Find me',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId

    const found = await getAutonomyFlowById(flowId, tempDir)
    expect(found).not.toBeNull()
    expect(found!.flowId).toBe(flowId)
    expect(found!.goal).toBe('Find me')
  })
})

describe('queueManagedAutonomyFlowStepRun edge cases', () => {
  test('returns null for unknown flowId', async () => {
    const result = await queueManagedAutonomyFlowStepRun({
      flowId: 'nonexistent',
      stepId: 'step-0',
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 1000,
    })
    expect(result).toBeNull()
  })

  test('returns unchanged flow for mismatched stepId', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Mismatch test',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId

    const result = await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: 'wrong-step-id',
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })

    // Should return the flow unchanged (still pending, not queued step)
    expect(result).not.toBeNull()
    expect(result!.stateJson?.steps[0]?.status).toBe('pending')
  })
})

describe('markManagedAutonomyFlowStepRunning edge cases', () => {
  test('returns null for unknown flowId', async () => {
    const result = await markManagedAutonomyFlowStepRunning({
      flowId: 'nonexistent',
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 1000,
    })
    expect(result).toBeNull()
  })
})

describe('markManagedAutonomyFlowStepFailed with cancelRequestedAt', () => {
  test('marks flow as cancelled (not failed) when cancel was requested', async () => {
    const startResult = await startManagedAutonomyFlow({
      trigger: 'scheduled-task',
      goal: 'Fail after cancel',
      steps: TWO_STEPS,
      rootDir: tempDir,
      nowMs: 1000,
    })
    const flowId = startResult!.flow.flowId
    const step0Id = startResult!.nextStep!.step.stepId

    await queueManagedAutonomyFlowStepRun({
      flowId,
      stepId: step0Id,
      stepIndex: 0,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 2000,
    })
    await markManagedAutonomyFlowStepRunning({
      flowId,
      runId: 'run-0',
      rootDir: tempDir,
      nowMs: 3000,
    })

    // Request cancel while running
    await requestManagedAutonomyFlowCancel({
      flowId,
      rootDir: tempDir,
      nowMs: 4000,
    })

    // Step fails — should result in cancelled (because cancel was requested)
    const result = await markManagedAutonomyFlowStepFailed({
      flowId,
      runId: 'run-0',
      error: 'step error',
      rootDir: tempDir,
      nowMs: 5000,
    })

    expect(result!.flow.status).toBe('cancelled')
    expect(result!.flow.lastError).toBe('step error')
  })
})

describe('formatAutonomyFlowsStatus', () => {
  test('formats counts for various statuses', () => {
    const flows: AutonomyFlowRecord[] = [
      makeMinimalFlow({ status: 'queued' }),
      makeMinimalFlow({ status: 'running' }),
      makeMinimalFlow({ status: 'succeeded' }),
      makeMinimalFlow({ status: 'succeeded' }),
      makeMinimalFlow({ status: 'failed' }),
    ]

    const status = formatAutonomyFlowsStatus(flows)
    expect(status).toContain('Autonomy flows: 5')
    expect(status).toContain('Queued: 1')
    expect(status).toContain('Running: 1')
    expect(status).toContain('Succeeded: 2')
    expect(status).toContain('Failed: 1')
    expect(status).toContain('Cancelled: 0')
  })
})

describe('formatAutonomyFlowsList', () => {
  test('returns message when no flows', () => {
    const list = formatAutonomyFlowsList([])
    expect(list).toBe('No autonomy flows recorded.')
  })

  test('formats flow list with source and step info', () => {
    const flows: AutonomyFlowRecord[] = [
      makeMinimalFlow({
        flowId: 'flow-abc',
        goal: 'Test goal',
        currentStep: 'gather',
        sourceLabel: 'nightly',
        revision: 3,
        runCount: 2,
        status: 'queued',
      }),
    ]

    const list = formatAutonomyFlowsList(flows)
    expect(list).toContain('flow-abc')
    expect(list).toContain('nightly')
    expect(list).toContain('step=gather')
    expect(list).toContain('rev=3')
    expect(list).toContain('goal=Test goal')
    expect(list).toContain('runs=2')
  })

  test('respects limit parameter', () => {
    const flows = Array.from({ length: 5 }, (_, i) =>
      makeMinimalFlow({ flowId: `flow-${i}` }),
    )

    const list = formatAutonomyFlowsList(flows, 2)
    expect(list).toContain('flow-0')
    expect(list).toContain('flow-1')
    expect(list).not.toContain('flow-2')
  })

  test('shows waiting info for waiting flows', () => {
    const flows: AutonomyFlowRecord[] = [
      makeMinimalFlow({
        status: 'waiting',
        waitJson: {
          reason: 'manual-review',
          stepId: 's1',
          stepName: 'review',
          stepIndex: 1,
        },
      }),
    ]

    const list = formatAutonomyFlowsList(flows)
    expect(list).toContain('waiting=manual-review')
  })
})

describe('formatAutonomyFlowDetail', () => {
  test('returns not found for null', () => {
    expect(formatAutonomyFlowDetail(null)).toBe('Autonomy flow not found.')
    expect(formatAutonomyFlowDetail(undefined)).toBe('Autonomy flow not found.')
  })

  test('formats full flow detail with steps', () => {
    const flow = makeMinimalFlow({
      flowId: 'detail-flow',
      flowKey: 'managed:scheduled-task:src',
      revision: 2,
      trigger: 'scheduled-task',
      status: 'running',
      goal: 'Detail test',
      sourceLabel: 'nightly',
      ownerKey: 'main-thread',
      currentStep: 'gather',
      runCount: 1,
      latestRunId: 'run-0',
      stateJson: {
        currentStepIndex: 0,
        steps: [
          {
            stepId: 's0',
            name: 'gather',
            prompt: 'Gather inputs',
            status: 'running',
            runId: 'run-0',
          },
          {
            stepId: 's1',
            name: 'draft',
            prompt: 'Draft report',
            status: 'pending',
            waitFor: 'approval',
          },
        ],
      },
    })

    const detail = formatAutonomyFlowDetail(flow)
    expect(detail).toContain('Flow: detail-flow')
    expect(detail).toContain('Key: managed:scheduled-task:src')
    expect(detail).toContain('Mode: managed')
    expect(detail).toContain('Revision: 2')
    expect(detail).toContain('Status: running')
    expect(detail).toContain('Goal: Detail test')
    expect(detail).toContain('Source: nightly')
    expect(detail).toContain('Current step: gather')
    expect(detail).toContain('1. gather | running | run=run-0')
    expect(detail).toContain('2. draft | pending | run=none | wait=approval')
  })

  test('includes error and blocked info', () => {
    const flow = makeMinimalFlow({
      status: 'failed',
      lastError: 'step exploded',
      blockedRunId: 'run-x',
      blockedSummary: 'step exploded',
    })

    const detail = formatAutonomyFlowDetail(flow)
    expect(detail).toContain('Error: step exploded')
    expect(detail).toContain('Blocked run: run-x')
    expect(detail).toContain('Blocked summary: step exploded')
  })

  test('includes cancel requested timestamp', () => {
    const flow = makeMinimalFlow({
      cancelRequestedAt: 99999,
    })
    const detail = formatAutonomyFlowDetail(flow)
    expect(detail).toContain('Cancel requested:')
  })
})

describe('concurrent startManagedAutonomyFlow calls', () => {
  test('do not lose updates', async () => {
    await Promise.all([
      startManagedAutonomyFlow({
        trigger: 'scheduled-task',
        goal: 'Flow A',
        steps: [{ name: 'a', prompt: 'A' }],
        rootDir: tempDir,
        sourceId: 'src-a',
        nowMs: 1000,
      }),
      startManagedAutonomyFlow({
        trigger: 'scheduled-task',
        goal: 'Flow B',
        steps: [{ name: 'b', prompt: 'B' }],
        rootDir: tempDir,
        sourceId: 'src-b',
        nowMs: 1000,
      }),
    ])

    const flows = await listAutonomyFlows(tempDir)
    expect(flows).toHaveLength(2)
    const goals = new Set(flows.map(f => f.goal))
    expect(goals).toEqual(new Set(['Flow A', 'Flow B']))
  })
})

// Helper to make minimal flow records for formatter tests
function makeMinimalFlow(
  overrides: Partial<AutonomyFlowRecord> = {},
): AutonomyFlowRecord {
  return {
    flowId: 'flow-0',
    flowKey: 'managed:scheduled-task:src',
    syncMode: 'managed',
    ownerKey: DEFAULT_AUTONOMY_OWNER_KEY,
    revision: 1,
    trigger: 'scheduled-task',
    status: 'queued',
    goal: 'Default goal',
    rootDir: '/tmp/test',
    currentDir: '/tmp/test',
    runCount: 0,
    createdAt: 1000,
    updatedAt: 2000,
    ...overrides,
  }
}
