/**
 * Tests for src/core/skillpack/init-scaffold.ts +
 * src/core/skillpack/pack-publish.ts — the publisher side.
 */
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { spawnSync } from 'child_process';
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';

import {
  InitScaffoldError,
  runInitScaffold,
} from '../src/core/skillpack/init-scaffold.ts';
import {
  PackPublishError,
  runPackPublish,
} from '../src/core/skillpack/pack-publish.ts';
import { loadSkillpackManifest } from '../src/core/skillpack/manifest-v1.ts';
import { runDoctor } from '../src/core/skillpack/doctor.ts';

function hasGnuTar(): boolean {
  for (const bin of ['gtar', 'tar']) {
    const r = spawnSync(bin, ['--version'], { encoding: 'utf-8' });
    if (r.status === 0 && r.stdout.includes('GNU')) return true;
  }
  return false;
}
const SKIP_NO_GNU = !hasGnuTar();

let tmp: string;
beforeAll(() => {
  tmp = mkdtempSync(join(tmpdir(), 'init-pack-'));
});
afterAll(() => {
  rmSync(tmp, { recursive: true, force: true });
});

describe('runInitScaffold — cathedral default', () => {
  test('scaffolds every file the rubric expects', () => {
    const dir = mkdtempSync(join(tmp, 'init-cathedral-'));
    const result = runInitScaffold({ targetDir: dir, name: 'test-pack' });
    for (const expected of [
      'skillpack.json',
      'skills/test-pack/SKILL.md',
      'skills/test-pack/routing-eval.jsonl',
      'runbooks/bootstrap.md',
      'CHANGELOG.md',
      'README.md',
      'LICENSE',
      '.gitignore',
      'test/example.test.ts',
      'e2e/example.e2e.test.ts',
      'evals/test-pack.judge.json',
    ]) {
      expect(existsSync(join(dir, expected))).toBe(true);
    }
    expect(result.filesWritten.length).toBeGreaterThan(8);
    expect(result.manifest.name).toBe('test-pack');
  });

  test('--minimal omits test / e2e / evals', () => {
    const dir = mkdtempSync(join(tmp, 'init-min-'));
    runInitScaffold({ targetDir: dir, name: 'mini-pack', minimal: true });
    expect(existsSync(join(dir, 'skillpack.json'))).toBe(true);
    expect(existsSync(join(dir, 'test/example.test.ts'))).toBe(false);
    expect(existsSync(join(dir, 'e2e/example.e2e.test.ts'))).toBe(false);
    expect(existsSync(join(dir, 'evals/mini-pack.judge.json'))).toBe(false);
  });

  test('refuses to overwrite existing files', () => {
    const dir = mkdtempSync(join(tmp, 'init-overwrite-'));
    mkdirSync(join(dir, 'skills/preexist'), { recursive: true });
    writeFileSync(join(dir, 'skills/preexist/SKILL.md'), 'user content');
    const result = runInitScaffold({ targetDir: dir, name: 'preexist' });
    // The user's SKILL.md should be in filesSkippedExisting
    // filesSkippedExisting holds join()-built paths — '\' on win32.
    expect(
      result.filesSkippedExisting.some((p) => p.endsWith(join('skills', 'preexist', 'SKILL.md'))),
    ).toBe(true);
    // And contents preserved.
    expect(require('fs').readFileSync(join(dir, 'skills/preexist/SKILL.md'), 'utf-8')).toBe('user content');
  });

  test('rejects invalid kebab name', () => {
    try {
      runInitScaffold({ targetDir: tmp, name: 'UpperCase' });
      throw new Error('should have thrown');
    } catch (err) {
      expect((err as InitScaffoldError).code).toBe('invalid_name');
    }
  });

  test('--dry-run writes nothing but reports plan', () => {
    const dir = mkdtempSync(join(tmp, 'init-dry-'));
    const result = runInitScaffold({ targetDir: dir, name: 'dry-pack', dryRun: true });
    expect(result.filesWritten.length).toBeGreaterThan(0);
    expect(existsSync(join(dir, 'skillpack.json'))).toBe(false);
  });

  test('freshly scaffolded pack scores 10/10 on doctor --quick', async () => {
    const dir = mkdtempSync(join(tmp, 'init-tenten-'));
    runInitScaffold({ targetDir: dir, name: 'reference' });
    const r = await runDoctor({ packRoot: dir, mode: 'quick' });
    expect(r.score).toBe(10);
    expect(r.tier_eligibility).toBe('endorsed');
  });
});

describe('runPackPublish — happy path', () => {
  test.skipIf(SKIP_NO_GNU)('emits a deterministic tarball when doctor passes', async () => {
    const dir = mkdtempSync(join(tmp, 'pack-good-'));
    runInitScaffold({ targetDir: dir, name: 'pack-good' });
    const result = await runPackPublish({ packRoot: dir });
    expect(result.refused_reason).toBeNull();
    expect(result.tarball).not.toBeNull();
    expect(result.tarball?.outPath).toContain('pack-good-0.1.0.tgz');
    expect(result.tarball?.sha256.length).toBe(64);
    expect(result.tarball?.fileCount).toBeGreaterThan(0);
    expect(result.tarball?.tier_eligibility).toBe('endorsed');
    expect(existsSync(result.tarball!.outPath)).toBe(true);
  });

  test('refuses to pack when doctor reports blocked tier', async () => {
    const dir = mkdtempSync(join(tmp, 'pack-bad-'));
    runInitScaffold({ targetDir: dir, name: 'pack-bad' });
    // Sabotage core dimension 5 by removing CHANGELOG.
    rmSync(join(dir, 'CHANGELOG.md'));
    const result = await runPackPublish({ packRoot: dir });
    expect(result.tarball).toBeNull();
    expect(result.refused_reason).toContain('changelog_present_and_current');
  });

  test('dry-run skips tarball but runs doctor', async () => {
    const dir = mkdtempSync(join(tmp, 'pack-dry-'));
    runInitScaffold({ targetDir: dir, name: 'pack-dry' });
    const result = await runPackPublish({ packRoot: dir, dryRun: true });
    expect(result.tarball).toBeNull();
    expect(result.doctor?.score).toBe(10);
  });

  test('throws when skillpack.json missing', async () => {
    const dir = mkdtempSync(join(tmp, 'pack-missing-'));
    try {
      await runPackPublish({ packRoot: dir });
      throw new Error('should have thrown');
    } catch (err) {
      expect((err as PackPublishError).code).toBe('manifest_load_failed');
    }
  });

  test.skipIf(SKIP_NO_GNU)('--skip-doctor packs without running the gate', async () => {
    const dir = mkdtempSync(join(tmp, 'pack-skipdoc-'));
    runInitScaffold({ targetDir: dir, name: 'pack-skipdoc' });
    const result = await runPackPublish({ packRoot: dir, skipDoctor: true });
    expect(result.doctor).toBeNull();
    expect(result.tarball).not.toBeNull();
  });
});

describe('end-to-end init -> doctor -> pack', () => {
  test.skipIf(SKIP_NO_GNU)('full publisher loop on a fresh pack', async () => {
    const dir = mkdtempSync(join(tmp, 'e2e-'));
    // 1. init
    runInitScaffold({ targetDir: dir, name: 'e2e-pack' });
    // 2. doctor
    const doc = await runDoctor({ packRoot: dir, mode: 'quick' });
    expect(doc.score).toBe(10);
    // 3. pack
    const pkg = await runPackPublish({ packRoot: dir });
    expect(pkg.tarball).not.toBeNull();
    // 4. manifest round-trip
    const m = loadSkillpackManifest(dir);
    expect(m.name).toBe('e2e-pack');
  });
});
