import { describe, it, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { join } from 'node:path';
import { mkdir, writeFile, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import {
  discoverConfigFiles,
  discoverConfigFilesMulti,
  discoverFullMachinePaths,
  readTextFile,
} from '../../scanners/lib/file-discovery.mjs';

/**
 * Create a temp directory with a unique name for test isolation.
 */
function tempDir(suffix) {
  return join(tmpdir(), `config-audit-fd-test-${suffix}-${Date.now()}`);
}

// ───────────────────────────────────────────────────────────────
// Group 1: discoverConfigFiles — single path
// ───────────────────────────────────────────────────────────────

describe('discoverConfigFiles (single path)', () => {
  let dir;

  before(async () => {
    dir = tempDir('single');
    // Create a realistic project structure
    await mkdir(join(dir, '.claude', 'rules'), { recursive: true });
    await mkdir(join(dir, 'hooks'), { recursive: true });
    await mkdir(join(dir, 'node_modules', 'pkg'), { recursive: true });
    await mkdir(join(dir, 'src'), { recursive: true });

    await writeFile(join(dir, 'CLAUDE.md'), '# Instructions');
    await writeFile(join(dir, '.claude', 'settings.json'), '{}');
    await writeFile(join(dir, '.claude', 'rules', 'my-rule.md'), '---\nglobs: "*.ts"\n---\nRule');
    await writeFile(join(dir, '.mcp.json'), '{"mcpServers": {}}');
    await writeFile(join(dir, 'hooks', 'hooks.json'), '{"hooks": {}}');
    await writeFile(join(dir, 'src', 'index.ts'), 'export {}');
    // Should be skipped
    await writeFile(join(dir, 'node_modules', 'pkg', 'CLAUDE.md'), '# dep');
  });

  after(async () => {
    await rm(dir, { recursive: true, force: true });
  });

  it('returns { files, skipped } shape', async () => {
    const result = await discoverConfigFiles(dir);
    assert.ok(Array.isArray(result.files));
    assert.equal(typeof result.skipped, 'number');
  });

  it('finds CLAUDE.md', async () => {
    const result = await discoverConfigFiles(dir);
    const claude = result.files.find(f => f.type === 'claude-md');
    assert.ok(claude, 'should find CLAUDE.md');
    assert.equal(claude.relPath, 'CLAUDE.md');
  });

  it('finds settings.json in .claude/', async () => {
    const result = await discoverConfigFiles(dir);
    const settings = result.files.find(f => f.type === 'settings-json');
    assert.ok(settings, 'should find settings.json');
    assert.ok(settings.relPath.includes('.claude'));
  });

  it('finds .mcp.json', async () => {
    const result = await discoverConfigFiles(dir);
    const mcp = result.files.find(f => f.type === 'mcp-json');
    assert.ok(mcp, 'should find .mcp.json');
  });

  it('finds rules in .claude/rules/', async () => {
    const result = await discoverConfigFiles(dir);
    const rules = result.files.filter(f => f.type === 'rule');
    assert.ok(rules.length > 0, 'should find at least one rule');
  });

  it('finds hooks.json', async () => {
    const result = await discoverConfigFiles(dir);
    const hooks = result.files.find(f => f.type === 'hooks-json');
    assert.ok(hooks, 'should find hooks.json');
  });

  it('skips node_modules', async () => {
    const result = await discoverConfigFiles(dir);
    const inNodeModules = result.files.filter(f => f.absPath.includes('node_modules'));
    assert.equal(inNodeModules.length, 0, 'should not include files in node_modules');
  });

  it('tracks skipped directories (counter fix)', async () => {
    const result = await discoverConfigFiles(dir);
    assert.ok(result.skipped >= 1, 'should count at least node_modules as skipped');
  });

  it('does not include non-config files', async () => {
    const result = await discoverConfigFiles(dir);
    const ts = result.files.find(f => f.absPath.endsWith('.ts'));
    assert.equal(ts, undefined, 'should not include .ts files');
  });
});

// ───────────────────────────────────────────────────────────────
// Group 2: File classification
// ───────────────────────────────────────────────────────────────

describe('file classification via discoverConfigFiles', () => {
  let dir;

  before(async () => {
    dir = tempDir('classify');
    await mkdir(join(dir, '.claude-plugin'), { recursive: true });
    await mkdir(join(dir, 'agents'), { recursive: true });
    await mkdir(join(dir, 'commands'), { recursive: true });
    await mkdir(join(dir, 'skills', 'my-skill'), { recursive: true });
    await mkdir(join(dir, 'hooks'), { recursive: true });

    await writeFile(join(dir, '.claude-plugin', 'plugin.json'), '{}');
    await writeFile(join(dir, 'agents', 'test-agent.md'), '---\nname: test\n---');
    await writeFile(join(dir, 'commands', 'test-cmd.md'), '---\nname: test\n---');
    await writeFile(join(dir, 'skills', 'my-skill', 'SKILL.md'), '# Skill');
    await writeFile(join(dir, 'hooks', 'hooks.json'), '{}');
    await writeFile(join(dir, 'CLAUDE.local.md'), '# Local');
    // Not a config file
    await writeFile(join(dir, 'random.txt'), 'hello');
  });

  after(async () => {
    await rm(dir, { recursive: true, force: true });
  });

  it('classifies plugin.json in .claude-plugin/', async () => {
    const { files } = await discoverConfigFiles(dir);
    const plugin = files.find(f => f.type === 'plugin-json');
    assert.ok(plugin, 'should find plugin.json');
  });

  it('classifies agent .md in agents/', async () => {
    const { files } = await discoverConfigFiles(dir);
    const agent = files.find(f => f.type === 'agent-md');
    assert.ok(agent, 'should find agent markdown');
  });

  it('classifies command .md in commands/', async () => {
    const { files } = await discoverConfigFiles(dir);
    const cmd = files.find(f => f.type === 'command-md');
    assert.ok(cmd, 'should find command markdown');
  });

  it('classifies SKILL.md', async () => {
    const { files } = await discoverConfigFiles(dir);
    const skill = files.find(f => f.type === 'skill-md');
    assert.ok(skill, 'should find SKILL.md');
  });

  it('classifies hooks.json in hooks/', async () => {
    const { files } = await discoverConfigFiles(dir);
    const hooks = files.find(f => f.type === 'hooks-json');
    assert.ok(hooks, 'should find hooks.json');
  });

  it('classifies CLAUDE.local.md', async () => {
    const { files } = await discoverConfigFiles(dir);
    const local = files.find(f => f.absPath.endsWith('CLAUDE.local.md'));
    assert.ok(local, 'should find CLAUDE.local.md');
    assert.equal(local.type, 'claude-md');
  });

  it('does not discover random.txt', async () => {
    const { files } = await discoverConfigFiles(dir);
    const txt = files.find(f => f.absPath.endsWith('random.txt'));
    assert.equal(txt, undefined, 'should not discover .txt files');
  });
});

// ───────────────────────────────────────────────────────────────
// Group 3: Depth limit
// ───────────────────────────────────────────────────────────────

describe('depth limit', () => {
  let dir;

  before(async () => {
    dir = tempDir('depth');
    // Create structure: a/b/c/d/e/f/g/CLAUDE.md (depth 7 from root)
    const shallow = join(dir, 'a', 'b');
    const deep = join(dir, 'a', 'b', 'c', 'd', 'e', 'f', 'g');
    await mkdir(shallow, { recursive: true });
    await mkdir(deep, { recursive: true });
    await writeFile(join(shallow, 'CLAUDE.md'), '# Shallow (depth 2)');
    await writeFile(join(deep, 'CLAUDE.md'), '# Deep (depth 7)');
  });

  after(async () => {
    await rm(dir, { recursive: true, force: true });
  });

  it('finds deep files with default maxDepth (10)', async () => {
    const { files } = await discoverConfigFiles(dir);
    const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
    assert.ok(deep, 'should find CLAUDE.md at depth 7 with default maxDepth');
  });

  it('respects custom maxDepth: 3', async () => {
    const { files } = await discoverConfigFiles(dir, { maxDepth: 3 });
    const shallow = files.find(f => f.absPath.includes('a/b/CLAUDE.md'));
    const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
    assert.ok(shallow, 'should find shallow CLAUDE.md');
    assert.equal(deep, undefined, 'should NOT find deep CLAUDE.md with maxDepth: 3');
  });

  it('old depth limit of 5 would have missed depth-7 files', async () => {
    const { files } = await discoverConfigFiles(dir, { maxDepth: 5 });
    const deep = files.find(f => f.absPath.includes('f/g/CLAUDE.md'));
    assert.equal(deep, undefined, 'maxDepth: 5 should miss depth-7 files');
  });
});

// ───────────────────────────────────────────────────────────────
// Group 4: discoverConfigFilesMulti
// ───────────────────────────────────────────────────────────────

describe('discoverConfigFilesMulti', () => {
  let rootA, rootB;

  before(async () => {
    rootA = tempDir('multiA');
    rootB = tempDir('multiB');
    await mkdir(rootA, { recursive: true });
    await mkdir(rootB, { recursive: true });
    await mkdir(join(rootB, 'a', 'b', 'c', 'd'), { recursive: true });
    await mkdir(join(rootA, 'node_modules'), { recursive: true });

    await writeFile(join(rootA, 'CLAUDE.md'), '# Project A');
    await writeFile(join(rootA, '.mcp.json'), '{}');
    await writeFile(join(rootB, 'CLAUDE.md'), '# Project B');
    await writeFile(join(rootB, 'a', 'b', 'c', 'd', 'CLAUDE.md'), '# Deep B');
    // Skippable
    await writeFile(join(rootA, 'node_modules', 'CLAUDE.md'), '# Skip');
  });

  after(async () => {
    await rm(rootA, { recursive: true, force: true });
    await rm(rootB, { recursive: true, force: true });
  });

  it('discovers files from both roots', async () => {
    const roots = [
      { path: rootA, maxDepth: 10 },
      { path: rootB, maxDepth: 10 },
    ];
    const { files } = await discoverConfigFilesMulti(roots);
    const fromA = files.find(f => f.absPath.includes(rootA) && f.type === 'claude-md');
    const fromB = files.find(f => f.absPath === join(rootB, 'CLAUDE.md'));
    assert.ok(fromA, 'should find CLAUDE.md from root A');
    assert.ok(fromB, 'should find CLAUDE.md from root B');
  });

  it('deduplicates when same root listed twice', async () => {
    const roots = [
      { path: rootA, maxDepth: 10 },
      { path: rootA, maxDepth: 10 },
    ];
    const { files } = await discoverConfigFilesMulti(roots);
    const claudeFiles = files.filter(f => f.absPath === join(rootA, 'CLAUDE.md'));
    assert.equal(claudeFiles.length, 1, 'should deduplicate same file');
  });

  it('accumulates skipped count across roots', async () => {
    const roots = [
      { path: rootA, maxDepth: 10 },
      { path: rootB, maxDepth: 10 },
    ];
    const { skipped } = await discoverConfigFilesMulti(roots);
    assert.ok(skipped >= 1, 'should accumulate skipped dirs from rootA');
  });

  it('respects per-root maxDepth', async () => {
    const roots = [
      { path: rootA, maxDepth: 10 },
      { path: rootB, maxDepth: 2 },  // blocks depth-4 file in rootB
    ];
    const { files } = await discoverConfigFilesMulti(roots);
    const deepB = files.find(f => f.absPath.includes('c/d/CLAUDE.md'));
    assert.equal(deepB, undefined, 'should not find deep file when maxDepth is 2');
  });

  it('respects global maxFiles cap', async () => {
    const roots = [
      { path: rootA, maxDepth: 10 },
      { path: rootB, maxDepth: 10 },
    ];
    const { files } = await discoverConfigFilesMulti(roots, { maxFiles: 1 });
    assert.equal(files.length, 1, 'should stop after maxFiles');
  });
});

// ───────────────────────────────────────────────────────────────
// Group 5: discoverFullMachinePaths
// ───────────────────────────────────────────────────────────────

describe('discoverFullMachinePaths', () => {
  it('returns an array', async () => {
    const paths = await discoverFullMachinePaths();
    assert.ok(Array.isArray(paths));
  });

  it('each entry has path (string) and maxDepth (number)', async () => {
    const paths = await discoverFullMachinePaths();
    for (const entry of paths) {
      assert.equal(typeof entry.path, 'string', 'path should be string');
      assert.equal(typeof entry.maxDepth, 'number', 'maxDepth should be number');
    }
  });

  it('only returns existing directories', async () => {
    const paths = await discoverFullMachinePaths();
    for (const entry of paths) {
      const s = await stat(entry.path);
      assert.ok(s.isDirectory(), `${entry.path} should be a directory`);
    }
  });

  it('includes ~/.claude if it exists', async () => {
    const home = process.env.HOME || '';
    const paths = await discoverFullMachinePaths();
    const hasClaude = paths.some(p => p.path === join(home, '.claude'));
    // Only assert if ~/.claude exists on this machine
    try {
      await stat(join(home, '.claude'));
      assert.ok(hasClaude, 'should include ~/.claude');
    } catch {
      // ~/.claude doesn't exist, skip
    }
  });

  it('has no duplicate paths', async () => {
    const paths = await discoverFullMachinePaths();
    const seen = new Set();
    for (const entry of paths) {
      assert.ok(!seen.has(entry.path), `duplicate path: ${entry.path}`);
      seen.add(entry.path);
    }
  });

  it('~/.claude gets maxDepth >= 6', async () => {
    const home = process.env.HOME || '';
    const paths = await discoverFullMachinePaths();
    const claude = paths.find(p => p.path === join(home, '.claude'));
    if (claude) {
      assert.ok(claude.maxDepth >= 6, 'maxDepth for ~/.claude should be >= 6');
    }
  });
});

// ───────────────────────────────────────────────────────────────
// Group 6: readTextFile
// ───────────────────────────────────────────────────────────────

describe('readTextFile', () => {
  let dir;

  before(async () => {
    dir = tempDir('readtext');
    await mkdir(dir, { recursive: true });
    await writeFile(join(dir, 'good.md'), '# Hello\nWorld');
    await writeFile(join(dir, 'binary.bin'), Buffer.from([0x48, 0x65, 0x00, 0x6c, 0x6f]));
  });

  after(async () => {
    await rm(dir, { recursive: true, force: true });
  });

  it('returns string for valid UTF-8 file', async () => {
    const content = await readTextFile(join(dir, 'good.md'));
    assert.equal(typeof content, 'string');
    assert.ok(content.includes('Hello'));
  });

  it('returns null for binary file (null bytes)', async () => {
    const content = await readTextFile(join(dir, 'binary.bin'));
    assert.equal(content, null);
  });

  it('returns null for nonexistent file', async () => {
    const content = await readTextFile(join(dir, 'nope.txt'));
    assert.equal(content, null);
  });
});
