// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * Edge Case Hunter — Boundary condition and failure mode analysis layer.
 *
 * Focuses on code that handles (or fails to handle) edge cases:
 * missing validation, unchecked returns, resource leaks, race conditions,
 * error swallowing, and unhandled promise rejections.
 *
 * @module review/layers/edge-case-hunter
 */

import { ReviewFinding, Severity } from '../engine.js';

// ---------------------------------------------------------------------------
// Pattern definitions
// ---------------------------------------------------------------------------

/** @type {import('./blind-hunter.js').VulnPattern[]} */
const PATTERNS = [
  // ── Error Handling ────────────────────────────────────────────────────
  {
    id: 'EC-ERR-001',
    title: 'Empty catch block — error swallowed',
    pattern: /catch\s*\([^)]*\)\s*\{\s*\}/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-390'],
    description: 'Empty catch block silently swallows errors, hiding failures and making debugging impossible.',
    remediation: 'Log the error, rethrow it, or handle it explicitly. At minimum: console.error(err).',
    languages: ['javascript', 'typescript', 'java', 'csharp'],
    tags: ['reliability'],
  },
  {
    id: 'EC-ERR-002',
    title: 'Catch-all with no logging',
    pattern: /catch\s*\(\s*(?:_|e|err|error|ex)\s*\)\s*\{\s*(?:\/\/|return|next\(|res\.)/g,
    severity: Severity.LOW,
    cweIds: ['CWE-390'],
    description: 'Catch block may not log the error before returning or continuing.',
    remediation: 'Ensure errors are logged before being handled or suppressed.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability', 'observability'],
  },
  {
    id: 'EC-ERR-003',
    title: 'Unhandled promise — missing .catch() or try/catch',
    pattern: /(?:(?:new\s+Promise|\.then)\s*\([^)]*\))\s*(?:;|\n)(?!\s*\.catch)/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-755'],
    description: 'Promise chain without .catch() handler. Unhandled rejections crash Node.js.',
    remediation: 'Add .catch() handler or use async/await with try/catch.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability'],
  },
  {
    id: 'EC-ERR-004',
    title: 'Generic exception catch — bare except',
    pattern: /except\s*:/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-396'],
    description: 'Bare except catches SystemExit, KeyboardInterrupt, and all other exceptions.',
    remediation: 'Catch specific exception types: except ValueError, except IOError.',
    languages: ['python'],
    tags: ['reliability'],
  },

  // ── Input Validation ──────────────────────────────────────────────────
  {
    id: 'EC-VAL-001',
    title: 'Missing type check — direct property access on parameter',
    pattern: /(?:function|=>)\s*(?:\([^)]*\)|[a-zA-Z_$]+)\s*(?:=>|{)\s*\n\s*(?:return\s+)?(?:(?!if|typeof|instanceof|assert|\?\.))[a-zA-Z_$]+\.[a-zA-Z_$]+/g,
    severity: Severity.LOW,
    cweIds: ['CWE-20'],
    description: 'Property accessed on parameter without type/null check. May throw on undefined/null.',
    remediation: 'Add typeof/instanceof check, use optional chaining (?.), or validate input.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability'],
  },
  {
    id: 'EC-VAL-002',
    title: 'Missing array bounds check',
    pattern: /\[\s*(?:index|idx|i|pos|offset|n)\s*\]/g,
    severity: Severity.LOW,
    cweIds: ['CWE-129'],
    description: 'Array accessed with variable index without bounds checking.',
    remediation: 'Verify index is within [0, array.length) before access.',
    languages: ['javascript', 'typescript', 'python', 'java', 'c', 'cpp'],
    tags: ['reliability'],
    exclude: /for\s*\(|\.forEach|\.map|\.filter|\.reduce|\.length/,
  },
  {
    id: 'EC-VAL-003',
    title: 'parseInt without radix',
    pattern: /parseInt\s*\(\s*[^,)]+\s*\)/g,
    severity: Severity.LOW,
    cweIds: ['CWE-704'],
    description: 'parseInt() without radix parameter. Strings starting with "0" may be parsed as octal.',
    remediation: 'Always specify radix: parseInt(value, 10).',
    languages: ['javascript', 'typescript'],
    tags: ['reliability'],
    exclude: /,\s*\d+\s*\)/,
  },

  // ── Resource Management ───────────────────────────────────────────────
  {
    id: 'EC-RES-001',
    title: 'Resource leak — stream/handle opened without close',
    pattern: /(?:createReadStream|createWriteStream|open|connect|createConnection)\s*\(/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-404'],
    description: 'Resource opened but may not be properly closed on error paths.',
    remediation: 'Use try/finally or pipeline() to ensure cleanup. Consider using "using" declarations.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability', 'performance'],
    exclude: /\.pipe\(|pipeline\(|\.on\s*\(\s*['"]close|\.on\s*\(\s*['"]end|finally/,
  },
  {
    id: 'EC-RES-002',
    title: 'Resource leak — database connection without release',
    pattern: /(?:pool\.query|pool\.connect|getConnection)\s*\(/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-404'],
    description: 'Database connection acquired but may not be released on error paths.',
    remediation: 'Use try/finally to release connections, or use pool.query() which auto-releases.',
    languages: ['javascript', 'typescript', 'python', 'java'],
    tags: ['reliability', 'performance'],
    exclude: /finally|\.release|\.end|\.close|using/,
  },
  {
    id: 'EC-RES-003',
    title: 'Event listener leak — addEventListener without removal',
    pattern: /addEventListener\s*\(\s*['"][^'"]+['"]/g,
    severity: Severity.LOW,
    cweIds: ['CWE-401'],
    description: 'Event listener added but may not be removed, causing memory leaks.',
    remediation: 'Store reference and call removeEventListener in cleanup/destroy/unmount.',
    languages: ['javascript', 'typescript'],
    tags: ['performance', 'reliability'],
    exclude: /removeEventListener|AbortController|signal|once\s*:/,
  },
  {
    id: 'EC-RES-004',
    title: 'Timer leak — setInterval without clearInterval',
    pattern: /setInterval\s*\(/g,
    severity: Severity.LOW,
    cweIds: ['CWE-401'],
    description: 'setInterval() without corresponding clearInterval() can leak timers.',
    remediation: 'Store the interval ID and call clearInterval() in cleanup logic.',
    languages: ['javascript', 'typescript'],
    tags: ['performance', 'reliability'],
    exclude: /clearInterval|intervalId|interval_id|timer/i,
  },

  // ── Race Conditions / Concurrency ─────────────────────────────────────
  {
    id: 'EC-RACE-001',
    title: 'TOCTOU race — check-then-act on filesystem',
    pattern: /(?:existsSync|exists|access)\s*\([^)]+\)[\s\S]{0,50}(?:readFile|writeFile|unlink|mkdir|rmSync)/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-367'],
    description: 'Time-of-check to time-of-use race: file existence check followed by file operation.',
    remediation: 'Use atomic operations. Open the file directly and handle ENOENT/EEXIST errors.',
    languages: ['javascript', 'typescript', 'python'],
    tags: ['reliability', 'T1574'],
  },
  {
    id: 'EC-RACE-002',
    title: 'Shared state mutation without synchronization',
    pattern: /(?:let|var)\s+\w+\s*=\s*(?:0|''|""|\[\]|\{\}|null|false)[\s\S]{0,200}(?:async|Promise|setTimeout|setInterval)/g,
    severity: Severity.LOW,
    cweIds: ['CWE-362'],
    description: 'Mutable variable in outer scope accessed from async contexts without synchronization.',
    remediation: 'Use atomic operations, mutex/lock patterns, or restructure to avoid shared mutable state.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability'],
  },

  // ── Null/Undefined Safety ─────────────────────────────────────────────
  {
    id: 'EC-NULL-001',
    title: 'Potential null dereference — .length without guard',
    pattern: /(?:params|query|body|headers|result|data|response|items|rows|records)\s*\.\s*length/g,
    severity: Severity.LOW,
    cweIds: ['CWE-476'],
    description: 'Accessing .length on a value that may be null or undefined.',
    remediation: 'Add null check: if (value && value.length) or use optional chaining: value?.length.',
    languages: ['javascript', 'typescript'],
    tags: ['reliability'],
    exclude: /\?\.|if\s*\(|&&|typeof|!= null|!== null|!= undefined|!== undefined/,
  },

  // ── Dangerous Defaults ────────────────────────────────────────────────
  {
    id: 'EC-DEF-001',
    title: 'Mutable default argument',
    pattern: /def\s+\w+\s*\([^)]*(?:=\s*\[\]|=\s*\{\}|=\s*set\(\))/g,
    severity: Severity.MEDIUM,
    cweIds: ['CWE-665'],
    description: 'Mutable default argument in Python is shared across calls, causing unexpected behavior.',
    remediation: 'Use None as default and create the mutable object inside the function body.',
    languages: ['python'],
    tags: ['reliability'],
  },
];

// ---------------------------------------------------------------------------
// Edge Case Hunter review function
// ---------------------------------------------------------------------------

/**
 * Run Edge Case Hunter analysis against source files.
 *
 * @param {import('../engine.js').SourceFile[]} sources
 * @param {object} [options]
 * @returns {Promise<ReviewFinding[]>}
 */
export async function edgeCaseReview(sources, options = {}) {
  const findings = [];

  for (const source of sources) {
    const lines = source.content.split('\n');

    for (const pat of PATTERNS) {
      if (!pat.languages.includes('*') && !pat.languages.includes(source.language)) {
        continue;
      }

      for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const trimmed = line.trimStart();
        if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*')) continue;

        pat.pattern.lastIndex = 0;
        const match = pat.pattern.exec(line);
        if (!match) continue;

        if (pat.exclude) {
          pat.exclude.lastIndex = 0;
          if (pat.exclude.test(line)) continue;
        }

        findings.push(
          new ReviewFinding({
            title: pat.title,
            severity: pat.severity,
            layer: 'edge-case-hunter',
            file: source.path,
            line: i + 1,
            column: match.index + 1,
            description: pat.description,
            proof: line.trim().slice(0, 200),
            remediation: pat.remediation,
            cweIds: [...pat.cweIds],
            tags: pat.tags ? [...pat.tags] : [],
            language: source.language,
            meta: { patternId: pat.id },
          }),
        );
      }
    }
  }

  return findings;
}
