/**
 * test/fanout.test.ts (W5) — pure launch-command + branch-slug builders.
 *
 * The load-bearing case is shellSingleQuote: the prompt is typed as raw bytes into
 * a lane's shell before `claude` parses it, so it must become one literal argv
 * element — no shell metacharacter may execute.
 */

import { describe, it, expect } from 'vitest'

import {
  shellSingleQuote,
  sanitizePrompt,
  buildFanoutCmd,
  laneBranch,
  slugify,
  FANOUT_PROMPT_MAX,
} from '../public/fanout.js'

// Local mirror of validateBranchNameClient's rules (projects.ts) — inlined so this
// stays a pure node test (importing projects.js would drag in @xterm/xterm).
function isValidBranch(branch: string): boolean {
  if (!branch || branch.length > 250) return false
  if (/[\x00-\x20\x7f]/.test(branch)) return false
  if (branch.startsWith('-') || branch.includes('..') || branch.endsWith('.lock')) return false
  if (/[~^:?*[\\]/.test(branch)) return false
  if (branch.includes('@{')) return false
  if (branch.startsWith('/') || branch.endsWith('/') || branch.includes('//')) return false
  return true
}

describe('shellSingleQuote', () => {
  it('single-quotes a plain string', () => {
    expect(shellSingleQuote('fix bug')).toBe(`'fix bug'`)
  })

  it("escapes an embedded single quote with the '\\'' sequence", () => {
    expect(shellSingleQuote("it's ok")).toBe(`'it'\\''s ok'`)
  })

  it('neutralizes shell metacharacters as literal text', () => {
    // $(...), backticks, ;, && all sit inside single quotes → passed verbatim.
    const dangerous = 'a; rm -rf / && echo $(whoami) `id`'
    const quoted = shellSingleQuote(dangerous)
    expect(quoted.startsWith("'")).toBe(true)
    expect(quoted.endsWith("'")).toBe(true)
    // No unescaped single quote can break out of the quoting.
    expect(quoted.slice(1, -1).includes("'")).toBe(false)
  })
})

describe('sanitizePrompt', () => {
  it('collapses newlines (\\n, \\r\\n) to single spaces', () => {
    expect(sanitizePrompt('a\nb\r\nc')).toBe('a b c')
  })

  it('collapses a bare carriage return too (would submit a partial line)', () => {
    expect(sanitizePrompt('a\rb')).toBe('a b')
  })

  it('trims surrounding whitespace', () => {
    expect(sanitizePrompt('  hello  ')).toBe('hello')
  })

  it('caps the length at FANOUT_PROMPT_MAX', () => {
    const long = 'x'.repeat(FANOUT_PROMPT_MAX + 500)
    expect(sanitizePrompt(long)).toHaveLength(FANOUT_PROMPT_MAX)
  })
})

describe('buildFanoutCmd', () => {
  it('adds --permission-mode for a non-default mode and ends with \\r', () => {
    expect(buildFanoutCmd('fix bug', 'plan', true)).toBe(`claude --permission-mode plan 'fix bug'\r`)
  })

  it('omits the flag for the default mode', () => {
    expect(buildFanoutCmd('fix bug', 'default', true)).toBe(`claude 'fix bug'\r`)
  })

  it('downgrades auto to default when the server forbids it (SEC-M5)', () => {
    expect(buildFanoutCmd('fix bug', 'auto', false)).toBe(`claude 'fix bug'\r`)
  })

  it('honors auto when the server allows it', () => {
    expect(buildFanoutCmd('fix bug', 'auto', true)).toBe(`claude --permission-mode auto 'fix bug'\r`)
  })

  it('shell-quotes an injection-style prompt into one argv element', () => {
    const cmd = buildFanoutCmd('rm -rf $(pwd); echo hi', 'default', false)
    expect(cmd).toBe(`claude 'rm -rf $(pwd); echo hi'\r`)
  })

  it('collapses a multi-line prompt before quoting', () => {
    expect(buildFanoutCmd('line1\nline2', 'default', false)).toBe(`claude 'line1 line2'\r`)
  })
})

describe('laneBranch', () => {
  it('builds `${base}-lane-${i}`', () => {
    expect(laneBranch('feat-x', 3)).toBe('feat-x-lane-3')
  })

  it('produces a valid git branch name', () => {
    expect(isValidBranch(laneBranch('feat-x', 3))).toBe(true)
    expect(isValidBranch(laneBranch(slugify('Add dark mode!'), 1))).toBe(true)
  })
})

describe('slugify', () => {
  it('lowercases and dash-collapses non-alphanumeric runs', () => {
    expect(slugify('Add Dark Mode!')).toBe('add-dark-mode')
  })

  it('strips leading/trailing dashes', () => {
    expect(slugify('  ...hello...  ')).toBe('hello')
  })

  it('falls back to "fanout" for an empty/symbol-only prompt', () => {
    expect(slugify('!!!')).toBe('fanout')
    expect(slugify('')).toBe('fanout')
  })

  it('caps the slug length and stays a valid branch base', () => {
    const slug = slugify('a'.repeat(100))
    expect(slug.length).toBeLessThanOrEqual(40)
    expect(isValidBranch(slug)).toBe(true)
  })
})
