// tests/hooks/path-guard.test.mjs
// Step 18 (plan-v2) — pins pre-write-executor.mjs BLOCK rules so a future
// silent weakening of the BLOCK_RULES list shows up as test failures
// instead of slipping through code review.
//
// Coverage: every BLOCK rule named in pre-write-executor.mjs gets at least
// one test. Allowlist examples (regular file paths, lib modules) confirm
// the hook does not over-block.

import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { runHook } from '../helpers/hook-helper.mjs';

const HERE = dirname(fileURLToPath(import.meta.url));
const ROOT = join(HERE, '..', '..');
const PRE_WRITE = join(ROOT, 'hooks', 'scripts', 'pre-write-executor.mjs');
const HOME = process.env.HOME || process.env.USERPROFILE || '/tmp';

function writeInput(file_path, content = 'x') {
  return { tool_name: 'Write', tool_input: { file_path, content } };
}

// -----------------------------------------------------------------------
// BLOCK — Git hook injection (.git/hooks/)
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS .git/hooks/ writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput('/tmp/repo/.git/hooks/pre-commit'));
  assert.strictEqual(code, 2, 'BLOCK exit code 2 expected for .git/hooks/ writes');
  assert.match(stderr, /Git hook injection/, 'BLOCK message should reference the rule name');
});

test('pre-write-executor BLOCKS deeper .git/hooks/ paths', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.git/hooks/post-receive'));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// BLOCK — Claude settings self-modification
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS .claude/settings.json writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput('/some/repo/.claude/settings.json'));
  assert.strictEqual(code, 2);
  assert.match(stderr, /Claude settings/);
});

test('pre-write-executor BLOCKS .claude/settings.local.json writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.claude/settings.local.json'));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// BLOCK — Claude hooks self-modification
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS .claude/hooks/ writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput('/some/repo/.claude/hooks/some-hook.mjs'));
  assert.strictEqual(code, 2);
  assert.match(stderr, /Claude hooks/);
});

test('pre-write-executor BLOCKS .claude-plugin/ writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.claude-plugin/plugin.json'));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// BLOCK — Shell configuration files
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS ~/.zshrc writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput(`${HOME}/.zshrc`));
  assert.strictEqual(code, 2);
  assert.match(stderr, /Shell configuration/);
});

test('pre-write-executor BLOCKS ~/.bashrc writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput(`${HOME}/.bashrc`));
  assert.strictEqual(code, 2);
});

test('pre-write-executor BLOCKS ~/.zshenv writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput(`${HOME}/.zshenv`));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// BLOCK — SSH directory
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS ~/.ssh/ writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput(`${HOME}/.ssh/id_rsa`));
  assert.strictEqual(code, 2);
  assert.match(stderr, /SSH/);
});

test('pre-write-executor BLOCKS ~/.ssh/config writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput(`${HOME}/.ssh/config`));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// BLOCK — AWS credentials
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS ~/.aws/ writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput(`${HOME}/.aws/credentials`));
  assert.strictEqual(code, 2);
  assert.match(stderr, /AWS/);
});

// -----------------------------------------------------------------------
// BLOCK — GnuPG directory
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS ~/.gnupg/ writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput(`${HOME}/.gnupg/private-keys-v1.d/foo`));
  assert.strictEqual(code, 2);
  assert.match(stderr, /GnuPG/);
});

// -----------------------------------------------------------------------
// BLOCK — Environment files (.env)
// -----------------------------------------------------------------------
test('pre-write-executor BLOCKS .env writes', async () => {
  const { code, stderr } = await runHook(PRE_WRITE, writeInput('/some/repo/.env'));
  assert.strictEqual(code, 2);
  assert.match(stderr, /Environment files/);
});

test('pre-write-executor BLOCKS .env.production writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.env.production'));
  assert.strictEqual(code, 2);
});

test('pre-write-executor BLOCKS .env.local writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.env.local'));
  assert.strictEqual(code, 2);
});

// -----------------------------------------------------------------------
// ALLOW — legitimate paths must not be blocked (over-block regression)
// -----------------------------------------------------------------------
test('pre-write-executor ALLOWS legitimate lib module writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/lib/util/foo.mjs'));
  assert.strictEqual(code, 0, 'legitimate lib writes must not be blocked');
});

test('pre-write-executor ALLOWS test file writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/tests/lib/foo.test.mjs'));
  assert.strictEqual(code, 0);
});

test('pre-write-executor ALLOWS docs writes', async () => {
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/docs/architecture.md'));
  assert.strictEqual(code, 0);
});

test('pre-write-executor BLOCKS .env.template writes (current over-block behavior — pin)', async () => {
  // The current .env regex (/\/\.env(?:\.[a-zA-Z0-9]+)?$/) blocks .env.X for
  // ALL alphanumeric X, including the safe `.template` convention. This test
  // pins the over-block as a known limitation. Loosening the rule to permit
  // `.env.template` (e.g. via an allowlist) is fine — but it should be a
  // deliberate change, not a silent weakening of BLOCK_RULES. If this test
  // starts failing, that is the trigger to revisit the regex intentionally.
  const { code } = await runHook(PRE_WRITE, writeInput('/some/repo/.env.template'));
  assert.strictEqual(code, 2, 'current behavior pin: .env.template is blocked. If you intend to allow it, update both the hook and this test together.');
});

// -----------------------------------------------------------------------
// FAIL OPEN — malformed input must not crash the hook chain
// -----------------------------------------------------------------------
test('pre-write-executor fails open on missing file_path', async () => {
  const { code } = await runHook(PRE_WRITE, { tool_name: 'Write', tool_input: {} });
  assert.strictEqual(code, 0, 'missing file_path should fail open (exit 0)');
});

test('pre-write-executor fails open on malformed JSON', async () => {
  const { code } = await runHook(PRE_WRITE, 'not-json');
  assert.strictEqual(code, 0, 'malformed JSON should fail open (exit 0)');
});
