const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { execFileSync } = require('node:child_process');

const { stringifyDagYaml, parseDagYaml } = require('../../scripts/lib/yaml-parser');
const { TaskStatus } = require('../../scripts/lib/models');
const { getWorktreePath } = require('../../scripts/lib/worktree-paths');

const CLI = path.resolve(__dirname, '../../scripts/cli.js');

function runCli(cwd, args, { expectFailure = false } = {}) {
  try {
    const stdout = execFileSync('node', [CLI, ...args], {
      cwd,
      env: { ...process.env, CLAUDE_PROJECT_DIR: cwd },
      encoding: 'utf8',
    });
    return { stdout, exitCode: 0 };
  } catch (err) {
    if (!expectFailure) throw err;
    return { stdout: err.stdout || '', stderr: err.stderr || '', exitCode: err.status || 1 };
  }
}

function initRepo(root) {
  execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: root });
  execFileSync('git', ['config', 'user.email', 'test@x.com'], { cwd: root });
  execFileSync('git', ['config', 'user.name', 'Test'], { cwd: root });
  fs.writeFileSync(path.join(root, 'README.md'), 'x\n');
  execFileSync('git', ['add', 'README.md'], { cwd: root });
  execFileSync('git', ['commit', '-q', '-m', 'init'], { cwd: root });
}

function writeSpec(root, specId, epicIds) {
  const dir = path.join(root, 'specs', specId);
  fs.mkdirSync(dir, { recursive: true });
  const dag = {
    epics: epicIds.map((id) => ({
      id,
      name: id,
      spec_path: `epics/${id}/epic.md`,
      status: TaskStatus.PENDING,
      worktree: null,
      depends_on: [],
      features: [],
    })),
    blocked: [],
  };
  fs.writeFileSync(path.join(dir, 'dag.yaml'), stringifyDagYaml(dag));
}

describe('CLI multi-spec UX', () => {
  let root;
  beforeEach(() => {
    root = fs.mkdtempSync(path.join(os.tmpdir(), 'arcforge-cli-ms-'));
    initRepo(root);
  });
  afterEach(() => fs.rmSync(root, { recursive: true, force: true }));

  test('status with one spec returns flat shape (backwards compat)', () => {
    writeSpec(root, 'only-spec', ['epic-1']);
    const { stdout } = runCli(root, ['status', '--json']);
    const parsed = JSON.parse(stdout);
    expect(parsed).toHaveProperty('epics');
    expect(parsed).not.toHaveProperty('specs');
  });

  test('status with two specs aggregates under specs.<id>', () => {
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { stdout } = runCli(root, ['status', '--json']);
    const parsed = JSON.parse(stdout);
    expect(parsed).toHaveProperty('specs');
    expect(Object.keys(parsed.specs).sort()).toEqual(['spec-a', 'spec-b']);
    expect(parsed.specs['spec-a'].epics).toHaveLength(1);
  });

  test('status aggregation carries per-epic path (derived when worktree set, null otherwise)', () => {
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    // Mark spec-a's epic as expanded — status derives the absolute path
    // from the stored worktree value at read time (no real worktree dir
    // is needed for derivation).
    const dagPath = path.join(root, 'specs', 'spec-a', 'dag.yaml');
    const dag = parseDagYaml(fs.readFileSync(dagPath, 'utf8'));
    dag.epics[0].worktree = 'epic-a1';
    fs.writeFileSync(dagPath, stringifyDagYaml(dag));

    const { stdout } = runCli(root, ['status', '--json']);
    const parsed = JSON.parse(stdout);
    expect(parsed.specs['spec-a'].epics[0].path).toBe(getWorktreePath(root, 'spec-a', 'epic-a1'));
    expect(parsed.specs['spec-b'].epics[0].path).toBeNull();
  });

  test('next with two specs requires --spec-id', () => {
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { exitCode, stderr } = runCli(root, ['next'], { expectFailure: true });
    expect(exitCode).not.toBe(0);
    expect(stderr).toMatch(/Multiple specs/);
  });

  test('next with --spec-id picks the right spec', () => {
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { stdout } = runCli(root, ['next', '--spec-id', 'spec-b']);
    const parsed = JSON.parse(stdout);
    expect(parsed.id).toBe('epic-b1');
  });

  test('merge resolves by intersection when positional epics narrow to one spec', () => {
    // Codex P2: resolveMergeOrCleanupSpec must INTERSECT matches across epic
    // ids, not union. `epic-only-a` lives only in spec-a; `epic-shared` in
    // both. The command should resolve to spec-a, not fail as ambiguous.
    writeSpec(root, 'spec-a', ['epic-only-a', 'epic-shared']);
    writeSpec(root, 'spec-b', ['epic-shared']);
    // We don't need a real merge — trigger resolution by invoking merge with
    // the two positional epic ids. If resolution picks spec-b (wrong) or
    // errors as ambiguous, the test fails. "No completed epics" is an
    // expected downstream error — merge body runs only after resolution.
    const { exitCode, stderr } = runCli(root, ['merge', 'epic-only-a', 'epic-shared'], {
      expectFailure: true,
    });
    // Accepted outcome: resolution succeeded to spec-a (merge then fails on
    // "epic-only-a not completed" or similar — NOT on "span multiple specs").
    expect(stderr).not.toMatch(/span multiple specs/);
    expect(exitCode).not.toBe(0); // merge still exits non-zero for missing-completed-epic
  });

  test('sync --direction from-base in multi-spec mode errors out (not silent aggregate)', () => {
    // Codex P2 #3106674263: ambiguous-spec branch called syncAllSpecs
    // immediately, skipping --direction parsing/validation.
    // `arcforge sync --direction from-base` must fail loudly in multi-spec
    // mode, not silently aggregate — from-base/to-base/both are data-moving
    // directions and have no cross-spec semantics.
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { exitCode, stderr } = runCli(root, ['sync', '--direction', 'from-base'], {
      expectFailure: true,
    });
    expect(exitCode).not.toBe(0);
    expect(stderr).toMatch(/direction|--spec-id/);
  });

  test('sync --direction scan in multi-spec mode succeeds (equivalent to no flag)', () => {
    // Codex P2 #3106859042: the previous --direction guard was too broad
    // and rejected `scan` too. `scan` is the valid base-mode direction
    // (coord.sync auto-detects `'scan'` in base checkouts) and the multi-
    // spec aggregate IS effectively a cross-spec scan. `--direction scan`
    // must pass through, not error.
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { stdout } = runCli(root, ['sync', '--direction', 'scan', '--json']);
    const parsed = JSON.parse(stdout);
    expect(parsed).toHaveProperty('specs');
    expect(Object.keys(parsed.specs).sort()).toEqual(['spec-a', 'spec-b']);
  });

  test('reboot with two specs returns per-spec + totals', () => {
    writeSpec(root, 'spec-a', ['epic-a1']);
    writeSpec(root, 'spec-b', ['epic-b1']);
    const { stdout } = runCli(root, ['reboot', '--json']);
    const parsed = JSON.parse(stdout);
    expect(parsed).toHaveProperty('specs');
    expect(parsed).toHaveProperty('totals');
    expect(parsed.totals.remaining_count).toBe(0); // no features → 0
  });
});
