import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { spawnSync } from 'node:child_process';
import { resolve, join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { mkdir, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const PLUGIN_ROOT = resolve(__dirname, '../..');
const CLI = join(PLUGIN_ROOT, 'scanners', 'manifest.mjs');

function uniqueDir(suffix) {
  return join(tmpdir(), `config-audit-manifest-${suffix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
}

function runCli(args, env = {}) {
  const proc = spawnSync('node', [CLI, ...args], {
    encoding: 'utf-8',
    env: { ...process.env, ...env },
  });
  return proc;
}

describe('manifest CLI — real-config path (plugin root)', () => {
  let output;

  before(() => {
    const proc = runCli([PLUGIN_ROOT, '--json']);
    assert.equal(proc.status, 0, `expected exit 0, got ${proc.status}; stderr: ${proc.stderr}`);
    output = JSON.parse(proc.stdout);
  });

  it('emits non-empty sources array', () => {
    assert.ok(Array.isArray(output.sources));
    assert.ok(output.sources.length > 0,
      `expected sources.length > 0, got ${output.sources.length}`);
  });

  it('sources are sorted DESC by estimated_tokens', () => {
    for (let i = 1; i < output.sources.length; i++) {
      const prev = output.sources[i - 1].estimated_tokens;
      const curr = output.sources[i].estimated_tokens;
      assert.ok(prev >= curr,
        `sources[${i - 1}] (${prev}) should be >= sources[${i}] (${curr})`);
    }
  });

  it('total ≈ sum(sources.estimated_tokens) (within rounding tolerance)', () => {
    const sum = output.sources.reduce((s, x) => s + (x.estimated_tokens || 0), 0);
    assert.ok(output.total >= sum - 1 && output.total <= sum + 1,
      `expected total ≈ ${sum}, got ${output.total}`);
  });

  it('every source has kind/name/source/estimated_tokens', () => {
    for (const s of output.sources) {
      assert.ok(typeof s.kind === 'string' && s.kind.length > 0, 's.kind missing');
      assert.ok(typeof s.name === 'string' && s.name.length > 0, 's.name missing');
      assert.ok(typeof s.source === 'string', 's.source missing');
      assert.equal(typeof s.estimated_tokens, 'number', 's.estimated_tokens not a number');
    }
  });

  it('meta.repoPath matches the requested path', () => {
    assert.equal(output.meta.repoPath, PLUGIN_ROOT);
  });
});

describe('manifest CLI — fixture path (rich-repo with patched HOME)', () => {
  let fixture;

  before(async () => {
    fixture = await buildRichManifestRepo(uniqueDir('rich'));
  });

  after(async () => {
    if (fixture) await rm(fixture.root, { recursive: true, force: true });
  });

  it('discovers ≥5 sources (CLAUDE.md cascade + plugins + skills + MCP)', () => {
    const proc = runCli([fixture.root, '--json'], { HOME: fixture.fakeHome });
    assert.equal(proc.status, 0, `stderr: ${proc.stderr}`);
    const out = JSON.parse(proc.stdout);
    assert.ok(out.sources.length >= 5,
      `expected sources.length >= 5, got ${out.sources.length}: ${out.sources.map(s => `${s.kind}:${s.name}`).join(', ')}`);
  });

  it('includes both plugins (manifest-plugin-a + manifest-plugin-b)', () => {
    const proc = runCli([fixture.root, '--json'], { HOME: fixture.fakeHome });
    const out = JSON.parse(proc.stdout);
    const pluginNames = out.sources.filter(s => s.kind === 'plugin').map(s => s.name);
    assert.ok(pluginNames.includes('manifest-plugin-a'),
      `expected manifest-plugin-a in plugins; got: ${pluginNames.join(', ')}`);
    assert.ok(pluginNames.includes('manifest-plugin-b'),
      `expected manifest-plugin-b in plugins; got: ${pluginNames.join(', ')}`);
  });

  it('includes 3 fixture skills (alpha-skill, beta-skill, gamma-skill)', () => {
    const proc = runCli([fixture.root, '--json'], { HOME: fixture.fakeHome });
    const out = JSON.parse(proc.stdout);
    const skillNames = out.sources.filter(s => s.kind === 'skill').map(s => s.name);
    for (const expected of ['alpha-skill', 'beta-skill', 'gamma-skill']) {
      assert.ok(skillNames.includes(expected),
        `expected skill ${expected}; got skills: ${skillNames.join(', ')}`);
    }
  });

  it('includes the project .mcp.json server (manifest-mcp)', () => {
    const proc = runCli([fixture.root, '--json'], { HOME: fixture.fakeHome });
    const out = JSON.parse(proc.stdout);
    const mcpNames = out.sources.filter(s => s.kind === 'mcp-server').map(s => s.name);
    assert.ok(mcpNames.includes('manifest-mcp'),
      `expected manifest-mcp in mcp-servers; got: ${mcpNames.join(', ')}`);
  });
});

describe('manifest CLI — error handling', () => {
  it('exits 3 for nonexistent path', () => {
    const proc = runCli(['/nonexistent/path/should/not/exist', '--json']);
    assert.equal(proc.status, 3);
  });

  it('--output-file writes JSON to the path', async () => {
    const outPath = join(tmpdir(), `manifest-output-${Date.now()}.json`);
    try {
      const proc = runCli([PLUGIN_ROOT, '--output-file', outPath]);
      assert.equal(proc.status, 0);
      const { readFile } = await import('node:fs/promises');
      const content = await readFile(outPath, 'utf-8');
      const parsed = JSON.parse(content);
      assert.ok(Array.isArray(parsed.sources));
    } finally {
      await rm(outPath, { force: true });
    }
  });
});

/**
 * Build a richer fixture for manifest tests: 2 plugins + 3 skills + project
 * .mcp.json. Mirrors buildRichRepo from active-config-reader.test.mjs but
 * gives every plugin/skill a unique, recognizable name so assertions can be
 * substring-based instead of count-based.
 */
async function buildRichManifestRepo(root) {
  const fakeHome = join(root, 'fake-home');
  await mkdir(join(root, '.git'), { recursive: true });
  await writeFile(join(root, '.git', 'HEAD'), 'ref: refs/heads/main\n');

  await writeFile(
    join(root, 'CLAUDE.md'),
    '# Project\n\nManifest fixture.\n',
  );

  await mkdir(join(fakeHome, '.claude'), { recursive: true });
  await writeFile(
    join(fakeHome, '.claude', 'CLAUDE.md'),
    '# User\n\nFake home for manifest tests.\n',
  );

  await writeFile(
    join(root, '.mcp.json'),
    JSON.stringify({
      mcpServers: {
        'manifest-mcp': { command: 'npx', args: ['fake-pkg'] },
      },
    }, null, 2),
  );

  // Plugin A — has alpha-skill + beta-skill
  const pluginA = join(fakeHome, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins', 'manifest-plugin-a');
  await mkdir(join(pluginA, '.claude-plugin'), { recursive: true });
  await writeFile(
    join(pluginA, '.claude-plugin', 'plugin.json'),
    JSON.stringify({ name: 'manifest-plugin-a', version: '1.0.0', description: 'plugin a' }, null, 2),
  );
  await mkdir(join(pluginA, 'skills', 'alpha-skill'), { recursive: true });
  await writeFile(
    join(pluginA, 'skills', 'alpha-skill', 'SKILL.md'),
    '---\nname: alpha-skill\ndescription: alpha skill from plugin a\n---\n\nAlpha body.\n',
  );
  await mkdir(join(pluginA, 'skills', 'beta-skill'), { recursive: true });
  await writeFile(
    join(pluginA, 'skills', 'beta-skill', 'SKILL.md'),
    '---\nname: beta-skill\ndescription: beta skill from plugin a\n---\n\nBeta body.\n',
  );

  // Plugin B — has gamma-skill
  const pluginB = join(fakeHome, '.claude', 'plugins', 'marketplaces', 'mp', 'plugins', 'manifest-plugin-b');
  await mkdir(join(pluginB, '.claude-plugin'), { recursive: true });
  await writeFile(
    join(pluginB, '.claude-plugin', 'plugin.json'),
    JSON.stringify({ name: 'manifest-plugin-b', version: '1.0.0', description: 'plugin b' }, null, 2),
  );
  await mkdir(join(pluginB, 'skills', 'gamma-skill'), { recursive: true });
  await writeFile(
    join(pluginB, 'skills', 'gamma-skill', 'SKILL.md'),
    '---\nname: gamma-skill\ndescription: gamma skill from plugin b\n---\n\nGamma body.\n',
  );

  return { root, fakeHome };
}
