/**
 * Tests for S02 CLI surface — --output-format, exit codes, HeadlessJsonResult, --resume.
 *
 * Uses extracted parsing logic (mirrors headless.ts) and direct imports from
 * headless-types.ts / headless-events.ts to avoid transitive @singularity-forge/native
 * import that breaks in test environment.
 */

import test from 'node:test'
import assert from 'node:assert/strict'

// ─── Import exit code constants & mapStatusToExitCode ──────────────────────

import {
  EXIT_SUCCESS,
  EXIT_ERROR,
  EXIT_BLOCKED,
  EXIT_CANCELLED,
  mapStatusToExitCode,
} from '../headless-events.js'

import type { OutputFormat, HeadlessJsonResult } from '../headless-types.js'
import { VALID_OUTPUT_FORMATS } from '../headless-types.js'

// ─── Extracted parsing logic (mirrors headless.ts) ─────────────────────────

interface HeadlessOptions {
  timeout: number
  json: boolean
  outputFormat: OutputFormat
  model?: string
  command: string
  commandArgs: string[]
  context?: string
  contextText?: string
  auto?: boolean
  verbose?: boolean
  maxRestarts?: number
  supervised?: boolean
  responseTimeout?: number
  answers?: string
  eventFilter?: Set<string>
  resumeSession?: string
  bare?: boolean
}

function parseHeadlessArgs(argv: string[]): HeadlessOptions {
  const options: HeadlessOptions = {
    timeout: 300_000,
    json: false,
    outputFormat: 'text',
    command: 'auto',
    commandArgs: [],
  }

  const args = argv.slice(2)

  for (let i = 0; i < args.length; i++) {
    const arg = args[i]
    if (arg === 'headless') continue

    if (arg.startsWith('--')) {
      if (arg === '--timeout' && i + 1 < args.length) {
        options.timeout = parseInt(args[++i], 10)
      } else if (arg === '--json') {
        options.json = true
        options.outputFormat = 'stream-json'
      } else if (arg === '--output-format' && i + 1 < args.length) {
        const fmt = args[++i]
        if (!VALID_OUTPUT_FORMATS.has(fmt)) {
          throw new Error(`Invalid output format: ${fmt}`)
        }
        options.outputFormat = fmt as OutputFormat
        if (fmt === 'stream-json' || fmt === 'json') {
          options.json = true
        }
      } else if (arg === '--model' && i + 1 < args.length) {
        options.model = args[++i]
      } else if (arg === '--context' && i + 1 < args.length) {
        options.context = args[++i]
      } else if (arg === '--context-text' && i + 1 < args.length) {
        options.contextText = args[++i]
      } else if (arg === '--auto') {
        options.auto = true
      } else if (arg === '--verbose') {
        options.verbose = true
      } else if (arg === '--max-restarts' && i + 1 < args.length) {
        options.maxRestarts = parseInt(args[++i], 10)
      } else if (arg === '--answers' && i + 1 < args.length) {
        options.answers = args[++i]
      } else if (arg === '--events' && i + 1 < args.length) {
        options.eventFilter = new Set(args[++i].split(','))
        options.json = true
        if (options.outputFormat === 'text') {
          options.outputFormat = 'stream-json'
        }
      } else if (arg === '--supervised') {
        options.supervised = true
        options.json = true
        if (options.outputFormat === 'text') {
          options.outputFormat = 'stream-json'
        }
      } else if (arg === '--response-timeout' && i + 1 < args.length) {
        options.responseTimeout = parseInt(args[++i], 10)
      } else if (arg === '--resume' && i + 1 < args.length) {
        options.resumeSession = args[++i]
      } else if (arg === '--bare') {
        options.bare = true
      }
    } else if (options.command === 'auto') {
      options.command = arg
    } else {
      options.commandArgs.push(arg)
    }
  }

  return options
}

// ─── --output-format flag parsing ──────────────────────────────────────────

test('--output-format text sets outputFormat to text', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--output-format', 'text', 'auto'])
  assert.equal(opts.outputFormat, 'text')
  assert.equal(opts.json, false)
})

test('--output-format json sets outputFormat to json and json=true', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--output-format', 'json', 'auto'])
  assert.equal(opts.outputFormat, 'json')
  assert.equal(opts.json, true)
})

test('--output-format stream-json sets outputFormat to stream-json and json=true', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--output-format', 'stream-json', 'auto'])
  assert.equal(opts.outputFormat, 'stream-json')
  assert.equal(opts.json, true)
})

test('default output format is text', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', 'auto'])
  assert.equal(opts.outputFormat, 'text')
  assert.equal(opts.json, false)
})

test('invalid --output-format value throws', () => {
  assert.throws(
    () => parseHeadlessArgs(['node', 'sf', 'headless', '--output-format', 'yaml', 'auto']),
    /Invalid output format: yaml/,
  )
})

test('invalid --output-format value (empty) throws', () => {
  assert.throws(
    () => parseHeadlessArgs(['node', 'sf', 'headless', '--output-format', 'xml', 'auto']),
    /Invalid output format/,
  )
})

// ─── --json backward compatibility ─────────────────────────────────────────

test('--json is alias for --output-format stream-json', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--json', 'auto'])
  assert.equal(opts.outputFormat, 'stream-json')
  assert.equal(opts.json, true)
})

test('--json before --output-format json: last writer wins', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--json', '--output-format', 'json', 'auto'])
  assert.equal(opts.outputFormat, 'json')
  assert.equal(opts.json, true)
})

// ─── --resume flag ─────────────────────────────────────────────────────────

test('--resume parses session ID', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--resume', 'abc-123', 'auto'])
  assert.equal(opts.resumeSession, 'abc-123')
  assert.equal(opts.command, 'auto')
})

test('no --resume means undefined', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', 'auto'])
  assert.equal(opts.resumeSession, undefined)
})

// ─── Exit code constants ───────────────────────────────────────────────────

test('EXIT_SUCCESS is 0', () => {
  assert.equal(EXIT_SUCCESS, 0)
})

test('EXIT_ERROR is 1', () => {
  assert.equal(EXIT_ERROR, 1)
})

test('EXIT_BLOCKED is 10', () => {
  assert.equal(EXIT_BLOCKED, 10)
})

test('EXIT_CANCELLED is 11', () => {
  assert.equal(EXIT_CANCELLED, 11)
})

// ─── mapStatusToExitCode ───────────────────────────────────────────────────

test('mapStatusToExitCode: success → 0', () => {
  assert.equal(mapStatusToExitCode('success'), EXIT_SUCCESS)
})

test('mapStatusToExitCode: complete → 0', () => {
  assert.equal(mapStatusToExitCode('complete'), EXIT_SUCCESS)
})

test('mapStatusToExitCode: error → 1', () => {
  assert.equal(mapStatusToExitCode('error'), EXIT_ERROR)
})

test('mapStatusToExitCode: timeout → 1', () => {
  assert.equal(mapStatusToExitCode('timeout'), EXIT_ERROR)
})

test('mapStatusToExitCode: blocked → 10', () => {
  assert.equal(mapStatusToExitCode('blocked'), EXIT_BLOCKED)
})

test('mapStatusToExitCode: cancelled → 11', () => {
  assert.equal(mapStatusToExitCode('cancelled'), EXIT_CANCELLED)
})

test('mapStatusToExitCode: unknown status defaults to EXIT_ERROR', () => {
  assert.equal(mapStatusToExitCode('unknown'), EXIT_ERROR)
  assert.equal(mapStatusToExitCode(''), EXIT_ERROR)
})

// ─── HeadlessJsonResult type shape ─────────────────────────────────────────

test('HeadlessJsonResult satisfies expected shape', () => {
  // Type-level assertion: construct a valid object and verify it compiles.
  // At runtime, verify all required keys exist.
  const result: HeadlessJsonResult = {
    status: 'success',
    exitCode: 0,
    duration: 12345,
    cost: { total: 0.05, input_tokens: 1000, output_tokens: 500, cache_read_tokens: 200, cache_write_tokens: 100 },
    toolCalls: 15,
    events: 42,
  }
  assert.equal(result.status, 'success')
  assert.equal(result.exitCode, 0)
  assert.equal(typeof result.duration, 'number')
  assert.ok(result.cost)
  assert.equal(typeof result.cost.total, 'number')
  assert.equal(typeof result.cost.input_tokens, 'number')
  assert.equal(typeof result.cost.output_tokens, 'number')
  assert.equal(typeof result.cost.cache_read_tokens, 'number')
  assert.equal(typeof result.cost.cache_write_tokens, 'number')
  assert.equal(typeof result.toolCalls, 'number')
  assert.equal(typeof result.events, 'number')
})

test('HeadlessJsonResult accepts optional fields', () => {
  const result: HeadlessJsonResult = {
    status: 'blocked',
    exitCode: 10,
    sessionId: 'sess-abc',
    duration: 5000,
    cost: { total: 0, input_tokens: 0, output_tokens: 0, cache_read_tokens: 0, cache_write_tokens: 0 },
    toolCalls: 0,
    events: 1,
    milestone: 'M001',
    phase: 'planning',
    nextAction: 'fix blocker',
    artifacts: ['ROADMAP.md'],
    commits: ['abc1234'],
  }
  assert.equal(result.sessionId, 'sess-abc')
  assert.equal(result.milestone, 'M001')
  assert.deepEqual(result.artifacts, ['ROADMAP.md'])
  assert.deepEqual(result.commits, ['abc1234'])
})

// ─── VALID_OUTPUT_FORMATS set ──────────────────────────────────────────────

test('VALID_OUTPUT_FORMATS contains exactly text, json, stream-json', () => {
  assert.equal(VALID_OUTPUT_FORMATS.size, 3)
  assert.ok(VALID_OUTPUT_FORMATS.has('text'))
  assert.ok(VALID_OUTPUT_FORMATS.has('json'))
  assert.ok(VALID_OUTPUT_FORMATS.has('stream-json'))
})

// ─── Regression: existing flags still parse correctly ──────────────────────

test('--events still works with new outputFormat default', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--events', 'agent_end,tool_execution_start', 'auto'])
  assert.ok(opts.eventFilter instanceof Set)
  assert.equal(opts.eventFilter!.size, 2)
  assert.equal(opts.json, true)
  assert.equal(opts.outputFormat, 'stream-json')
})

test('--timeout still works', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--timeout', '60000', 'auto'])
  assert.equal(opts.timeout, 60000)
})

test('--supervised still works and implies stream-json', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--supervised', 'auto'])
  assert.equal(opts.supervised, true)
  assert.equal(opts.json, true)
  assert.equal(opts.outputFormat, 'stream-json')
})

test('--answers still works', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--answers', 'answers.json', 'auto'])
  assert.equal(opts.answers, 'answers.json')
})

test('positional command parsing still works', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', 'next'])
  assert.equal(opts.command, 'next')
})

test('combined flags parse correctly', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    '--output-format', 'json',
    '--timeout', '120000',
    '--resume', 'sess-xyz',
    '--verbose',
    'auto',
  ])
  assert.equal(opts.outputFormat, 'json')
  assert.equal(opts.json, true)
  assert.equal(opts.timeout, 120000)
  assert.equal(opts.resumeSession, 'sess-xyz')
  assert.equal(opts.verbose, true)
  assert.equal(opts.command, 'auto')
})

// ─── --bare flag ───────────────────────────────────────────────────────────

test('--bare sets bare to true', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--bare', 'auto'])
  assert.equal(opts.bare, true)
  assert.equal(opts.command, 'auto')
})

test('no --bare means bare is undefined', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', 'auto'])
  assert.equal(opts.bare, undefined)
})

test('--bare is a boolean flag (no value needed)', () => {
  const opts = parseHeadlessArgs(['node', 'sf', 'headless', '--bare', '--json', 'auto'])
  assert.equal(opts.bare, true)
  assert.equal(opts.json, true)
})

test('--bare combined with --output-format json', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    '--bare',
    '--output-format', 'json',
    'auto',
  ])
  assert.equal(opts.bare, true)
  assert.equal(opts.outputFormat, 'json')
  assert.equal(opts.json, true)
  assert.equal(opts.command, 'auto')
})

// ─── Command-first ordering (flags after command) ─────────────────────────

test('command before flags: new-milestone --context-text --auto --verbose', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    'new-milestone',
    '--context-text', 'build something cool',
    '--auto',
    '--verbose',
  ])
  assert.equal(opts.command, 'new-milestone')
  assert.equal(opts.contextText, 'build something cool')
  assert.equal(opts.auto, true)
  assert.equal(opts.verbose, true)
})

test('command before flags: next --json --timeout', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    'next',
    '--json',
    '--timeout', '60000',
  ])
  assert.equal(opts.command, 'next')
  assert.equal(opts.json, true)
  assert.equal(opts.timeout, 60000)
})

test('command between flags: --auto new-milestone --verbose', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    '--auto',
    'new-milestone',
    '--verbose',
  ])
  assert.equal(opts.command, 'new-milestone')
  assert.equal(opts.auto, true)
  assert.equal(opts.verbose, true)
})

test('--bare does not affect other flags', () => {
  const opts = parseHeadlessArgs([
    'node', 'sf', 'headless',
    '--bare',
    '--timeout', '60000',
    '--resume', 'sess-abc',
    'auto',
  ])
  assert.equal(opts.bare, true)
  assert.equal(opts.timeout, 60000)
  assert.equal(opts.resumeSession, 'sess-abc')
  assert.equal(opts.command, 'auto')
})
