import { jest, describe, test, expect, beforeEach } from '@jest/globals';
import { createMockLogger, createMockConfig } from '../../__test-utils__/mockFactories.js';

// Mock fs before import
const mockFsPromises = {
  stat: jest.fn(),
  readFile: jest.fn(),
  readdir: jest.fn()
};

jest.unstable_mockModule('fs', () => ({
  default: { promises: mockFsPromises, readFileSync: jest.fn(() => { throw new Error('no file'); }) },
  promises: mockFsPromises,
  readFileSync: jest.fn(() => { throw new Error('no file'); })
}));

// Mock constants
jest.unstable_mockModule('../../utilities/constants.js', () => ({
  TOOL_STATUS: { PENDING: 'pending', EXECUTING: 'executing', COMPLETED: 'completed', FAILED: 'failed' },
  OPERATION_STATUS: { NOT_FOUND: 'not_found' },
  ERROR_TYPES: {},
  SYSTEM_DEFAULTS: { MAX_TOOL_EXECUTION_TIME: 300000 }
}));

const { default: CodeMapTool } = await import('../codeMapTool.js');

describe('CodeMapTool', () => {
  let tool;
  let logger;
  const context = { projectDir: '/project', agentId: 'agent-1' };

  beforeEach(() => {
    jest.clearAllMocks();
    logger = createMockLogger();
    tool = new CodeMapTool({}, logger);
  });

  test('constructor sets metadata correctly', () => {
    expect(tool.id).toBe('code-map');
    expect(tool.requiresProject).toBe(true);
    expect(tool.isAsync).toBe(true);
  });

  test('getDescription mentions skeleton and read-range', () => {
    const desc = tool.getDescription();
    expect(desc).toContain('skeleton');
    expect(desc).toContain('read-range');
  });

  test('getRequiredParameters returns action', () => {
    expect(tool.getRequiredParameters()).toEqual(['action']);
  });

  test('parseParameters parses JSON content', () => {
    const content = JSON.stringify({
      action: 'skeleton',
      path: 'src/',
      level: 'B.0'
    });
    const result = tool.parseParameters(content);
    expect(result.action).toBe('skeleton');
    expect(result.path).toBe('src/');
    expect(result.level).toBe('B.0');
  });

  test('parseParameters parses nested parameters JSON', () => {
    const content = JSON.stringify({
      parameters: { action: 'read-range', filePath: 'index.js', startLine: 1, endLine: 10 }
    });
    const result = tool.parseParameters(content);
    expect(result.action).toBe('read-range');
    expect(result.filePath).toBe('index.js');
    expect(result.startLine).toBe(1);
    expect(result.endLine).toBe(10);
  });

  test('parseParameters parses XML content', () => {
    const content = '<action>skeleton</action><path>src/</path><level>A.0</level>';
    const result = tool.parseParameters(content);
    expect(result.action).toBe('skeleton');
    expect(result.path).toBe('src/');
    expect(result.level).toBe('A.0');
  });

  test('parseParameters returns parseError on bad JSON', () => {
    const result = tool.parseParameters('{ broken');
    expect(result).toHaveProperty('parseError');
  });

  test('customValidateParameters rejects missing action', () => {
    const result = tool.customValidateParameters({});
    expect(result.valid).toBe(false);
  });

  test('customValidateParameters rejects invalid action', () => {
    const result = tool.customValidateParameters({ action: 'invalid' });
    expect(result.valid).toBe(false);
  });

  test('customValidateParameters requires path for skeleton', () => {
    const result = tool.customValidateParameters({ action: 'skeleton' });
    expect(result.valid).toBe(false);
    expect(result.errors.some(e => e.includes('path'))).toBe(true);
  });

  test('customValidateParameters rejects invalid level', () => {
    const result = tool.customValidateParameters({ action: 'skeleton', path: 'src/', level: 'X.9' });
    expect(result.valid).toBe(false);
  });

  test('customValidateParameters requires filePath/startLine/endLine for read-range', () => {
    const result = tool.customValidateParameters({ action: 'read-range' });
    expect(result.valid).toBe(false);
    expect(result.errors.length).toBeGreaterThanOrEqual(3);
  });

  test('customValidateParameters rejects endLine < startLine', () => {
    const result = tool.customValidateParameters({
      action: 'read-range', filePath: 'a.js', startLine: 10, endLine: 5
    });
    expect(result.valid).toBe(false);
  });

  test('customValidateParameters rejects range exceeding max', () => {
    const result = tool.customValidateParameters({
      action: 'read-range', filePath: 'a.js', startLine: 1, endLine: 600
    });
    expect(result.valid).toBe(false);
  });

  test('customValidateParameters accepts valid skeleton params', () => {
    const result = tool.customValidateParameters({ action: 'skeleton', path: 'src/' });
    expect(result.valid).toBe(true);
  });

  test('execute skeleton on single JS file', async () => {
    const jsContent = [
      'import express from "express";',
      '',
      'export class App {',
      '  constructor() {}',
      '  start() {',
      '    console.log("started");',
      '  }',
      '}',
      '',
      'export function main() {',
      '  return new App();',
      '}'
    ].join('\n');

    mockFsPromises.stat.mockResolvedValue({
      isFile: () => true,
      isDirectory: () => false,
      size: jsContent.length
    });
    mockFsPromises.readFile.mockResolvedValue(jsContent);

    const result = await tool.execute(
      { action: 'skeleton', path: 'src/app.js', level: 'B.0' },
      context
    );

    expect(result.success).toBe(true);
    expect(result.action).toBe('skeleton');
    expect(result.totalFiles).toBeGreaterThanOrEqual(1);
    expect(result.totalEntries).toBeGreaterThanOrEqual(1);
  });

  test('execute skeleton on directory with JS files', async () => {
    // First stat: directory check
    mockFsPromises.stat
      .mockResolvedValueOnce({ isFile: () => false, isDirectory: () => true })  // path stat
      .mockResolvedValueOnce({ size: 100 }); // file stat

    // Discover files - readdir for root
    mockFsPromises.readdir.mockResolvedValueOnce([
      { name: 'index.js', isDirectory: () => false, isFile: () => true, isSymbolicLink: () => false }
    ]);

    mockFsPromises.readFile
      .mockRejectedValueOnce(new Error('no .gitignore'))  // _loadGitignoreRules
      .mockResolvedValueOnce('export function hello() { return 1; }\n'); // file content

    const result = await tool.execute(
      { action: 'skeleton', path: 'src/', level: 'A.0' },
      context
    );

    expect(result.success).toBe(true);
    expect(result.action).toBe('skeleton');
  });

  test('execute skeleton returns empty when no supported files', async () => {
    mockFsPromises.stat.mockResolvedValue({ isFile: () => false, isDirectory: () => true });
    mockFsPromises.readdir.mockResolvedValue([]);
    mockFsPromises.readFile.mockRejectedValue(new Error('no file'));

    const result = await tool.execute(
      { action: 'skeleton', path: 'empty/' },
      context
    );

    expect(result.success).toBe(true);
    expect(result.totalFiles).toBe(0);
    expect(result.message).toContain('No supported files');
  });

  test('execute skeleton throws for non-existent path', async () => {
    mockFsPromises.stat.mockRejectedValue(new Error('ENOENT'));

    await expect(tool.execute(
      { action: 'skeleton', path: 'missing/' },
      context
    )).rejects.toThrow('Path not found');
  });

  test('execute read-range returns formatted lines', async () => {
    const content = 'line1\nline2\nline3\nline4\nline5\n';
    mockFsPromises.readFile.mockResolvedValue(content);

    const result = await tool.execute(
      { action: 'read-range', filePath: 'src/index.js', startLine: 2, endLine: 4 },
      context
    );

    expect(result.success).toBe(true);
    expect(result.action).toBe('read-range');
    expect(result.linesReturned).toBe(3);
    expect(result.content).toContain('line2');
    expect(result.content).toContain('line3');
    expect(result.content).toContain('line4');
  });

  test('execute read-range throws when startLine exceeds file length', async () => {
    mockFsPromises.readFile.mockResolvedValue('line1\nline2\n');

    await expect(tool.execute(
      { action: 'read-range', filePath: 'a.js', startLine: 100, endLine: 110 },
      context
    )).rejects.toThrow('exceeds file length');
  });

  test('execute read-range throws for missing file', async () => {
    mockFsPromises.readFile.mockRejectedValue(new Error('ENOENT'));

    await expect(tool.execute(
      { action: 'read-range', filePath: 'missing.js', startLine: 1, endLine: 5 },
      context
    )).rejects.toThrow('File not found');
  });

  test('execute throws on unknown action', async () => {
    await expect(tool.execute(
      { action: 'unknown' },
      context
    )).rejects.toThrow('Unknown action');
  });

  test('_langOf detects python files', () => {
    expect(tool._langOf('script.py')).toBe('python');
    expect(tool._langOf('app.js')).toBe('js');
    expect(tool._langOf('component.tsx')).toBe('js');
  });

  test('_parseJS extracts exported functions', () => {
    const lines = [
      'export function hello() {',
      '  return 1;',
      '}'
    ];
    const entries = tool._parseJS(lines, { publicOnly: true, withComments: false, includeImports: false });
    expect(entries.length).toBeGreaterThanOrEqual(1);
    expect(entries[0].kind).toBe('signature');
  });

  test('_parseJS extracts imports when includeImports is true', () => {
    const lines = [
      'import express from "express";',
      'const x = require("path");',
      'export function hello() {}'
    ];
    const entries = tool._parseJS(lines, { publicOnly: false, withComments: false, includeImports: true });
    const imports = entries.filter(e => e.kind === 'import');
    expect(imports.length).toBe(2);
  });

  test('_parseJS includes comments when withComments is true', () => {
    const lines = [
      '/** My doc */',
      'export function hello() {}'
    ];
    const entries = tool._parseJS(lines, { publicOnly: false, withComments: true, includeImports: false });
    const comments = entries.filter(e => e.kind === 'comment');
    expect(comments.length).toBeGreaterThanOrEqual(1);
  });

  test('_parsePython extracts def and class', () => {
    const lines = [
      'class MyClass:',
      '    def __init__(self):',
      '        pass',
      '',
      'def public_func():',
      '    return 1'
    ];
    const entries = tool._parsePython(lines, { publicOnly: false, withComments: false, includeImports: false });
    const sigs = entries.filter(e => e.kind === 'signature');
    expect(sigs.length).toBeGreaterThanOrEqual(2);
  });

  test('_parsePython respects publicOnly', () => {
    const lines = [
      'def public_func():',
      '    pass',
      'def _private_func():',
      '    pass'
    ];
    const entries = tool._parsePython(lines, { publicOnly: true, withComments: false, includeImports: false });
    const sigs = entries.filter(e => e.kind === 'signature');
    expect(sigs.length).toBe(1);
    expect(sigs[0].text).toContain('public_func');
  });

  test('_parseGitignore parses rules', () => {
    const content = '# comment\nnode_modules/\n*.log\n!important.log';
    const rules = tool._parseGitignore(content, '');
    expect(rules.length).toBe(3);
    expect(rules[2].negate).toBe(true);
  });

  test('_gitignorePatternToRegex handles ** patterns', () => {
    const re = tool._gitignorePatternToRegex('**/test');
    expect(re).toContain('(.+/)?');
  });

  // ─────────────────────────────────────────────────────────────────
  // Browser-JS / pre-ES-module patterns. Before this set of regexes,
  // CodeMap on a vanilla client-side .js file produced an empty map
  // because the parser only matched ESM exports + class methods.
  // These tests pin the new patterns. Each test exercises ONE pattern
  // in isolation so a future regression on any one of them surfaces
  // a precise failure.
  // ─────────────────────────────────────────────────────────────────
  describe('_parseJS — browser-JS patterns', () => {
    const opts = { publicOnly: false, withComments: false, includeImports: false };
    const signatures = (entries) =>
      entries.filter(e => e.kind === 'signature').map(e => e.text.trim());

    test('top-level IIFE is captured', () => {
      const entries = tool._parseJS([
        '(function () {',
        '  var x = 1;',
        '})();',
      ], opts);
      expect(signatures(entries)).toContain('(function () {');
    });

    test('IIFE with leading ! or + prefix (Webpack-UMD style)', () => {
      const entries = tool._parseJS([
        '!function () { return 1; }();',
        '+function () { return 2; }();',
      ], opts);
      expect(signatures(entries)).toEqual(expect.arrayContaining([
        '!function () { return 1; }();',
        '+function () { return 2; }();',
      ]));
    });

    test('window.X = function — global assignment', () => {
      const entries = tool._parseJS([
        'window.MyApp = function () {',
        '  return 1;',
        '};',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/window\.MyApp\s*=\s*function/);
    });

    test('globalThis.foo = () => — global arrow assignment', () => {
      const entries = tool._parseJS([
        'globalThis.greet = (name) => `hi ${name}`;',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/globalThis\.greet/);
    });

    test('Namespace.utils.foo = function — namespaced global assignment', () => {
      const entries = tool._parseJS([
        'MyApp.utils.formatDate = function (d) {',
        '  return d.toISOString();',
        '};',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/MyApp\.utils\.formatDate/);
    });

    test('Foo.prototype.bar = function — prototype method', () => {
      const entries = tool._parseJS([
        'Foo.prototype.bar = function () {',
        '  return this.x;',
        '};',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/Foo\.prototype\.bar/);
    });

    test('Foo.prototype.bar = () => — prototype arrow assignment', () => {
      // (Real-world prototype-as-arrow is unusual but the regex
      // covers it for completeness.)
      const entries = tool._parseJS([
        'Foo.prototype.bar = () => 42;',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/Foo\.prototype\.bar/);
    });

    test('jQuery $.fn.plugin = function — jQuery plugin pattern', () => {
      const entries = tool._parseJS([
        '$.fn.myPlugin = function (opts) {',
        '  return this.each(function () {});',
        '};',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/\$\.fn\.myPlugin/);
    });

    test('jQuery.fn.plugin = function — full jQuery prefix', () => {
      const entries = tool._parseJS([
        'jQuery.fn.banner = function () { return this; };',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/jQuery\.fn\.banner/);
    });

    test('document.addEventListener("DOMContentLoaded", …) — page lifecycle', () => {
      const entries = tool._parseJS([
        'document.addEventListener("DOMContentLoaded", () => {',
        '  init();',
        '});',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/document\.addEventListener/);
    });

    test('window.addEventListener("load", …) — page lifecycle (alt)', () => {
      const entries = tool._parseJS([
        'window.addEventListener("load", function () { boot(); });',
      ], opts);
      expect(signatures(entries).join('\n')).toMatch(/window\.addEventListener/);
    });

    test('REGRESSION: a vanilla browser file (IIFE wrapping jQuery plugins) now produces a non-empty map', () => {
      // This is the original failure case: pure client-side .js with
      // none of the new patterns produced zero entries. Pre-fix this
      // file's signatures.length would have been 0; post-fix it's > 0.
      const entries = tool._parseJS([
        '(function ($) {',
        '  $.fn.tooltip = function (options) {',
        '    return this.each(function () {});',
        '  };',
        '  $.fn.popover = function (options) {',
        '    return this.each(function () {});',
        '  };',
        '})(jQuery);',
      ], opts);
      expect(signatures(entries).length).toBeGreaterThanOrEqual(3);
    });

    test('REGRESSION: file unaffected by changes — modern ESM still matches', () => {
      // Make sure adding the new regexes didn't regress the original
      // matcher set.
      const entries = tool._parseJS([
        'export function hello() {}',
        'export class Greeter {}',
        'export const sum = (a, b) => a + b;',
      ], opts);
      expect(signatures(entries).length).toBe(3);
    });
  });

  // ─────────────────────────────────────────────────────────────────
  // TypeScript / TSX coverage. The JS parser is the same parser used
  // for .ts / .tsx / .mjs / .cjs (see _langOf); these tests pin which
  // TypeScript-specific patterns the no-regex parser captures TODAY
  // and which it MISSES, so a future tree-sitter migration (see the
  // file-header comment in codeMapTool.js) has an explicit baseline
  // to preserve / improve against.
  // ─────────────────────────────────────────────────────────────────
  describe('_parseJS — TypeScript / TSX coverage', () => {
    const opts = { publicOnly: false, withComments: false, includeImports: false };
    const sigs = (lines) =>
      tool._parseJS(lines, opts).filter(e => e.kind === 'signature').map(e => e.text.trim());

    // ── Captures we rely on (regressions here would break TS skeletons) ──

    test('export interface — one-line', () => {
      const out = sigs(['export interface User { id: string; name: string; }']);
      expect(out.join('\n')).toMatch(/export interface User/);
    });

    test('export type alias', () => {
      const out = sigs(['export type ID = string | number;']);
      expect(out.join('\n')).toMatch(/export type ID/);
    });

    test('export enum', () => {
      const out = sigs(['export enum Color { Red, Green, Blue }']);
      expect(out.join('\n')).toMatch(/export enum Color/);
    });

    test('export interface multi-line opening', () => {
      const out = sigs([
        'export interface User {',
        '  id: string;',
        '  name: string;',
        '}',
      ]);
      expect(out.join('\n')).toMatch(/export interface User \{/);
    });

    test('abstract class — declaration + abstract method', () => {
      const out = sigs([
        'abstract class Animal {',
        '  abstract sound(): string;',
        '}',
      ]);
      expect(out.join('\n')).toMatch(/abstract class Animal/);
      expect(out.join('\n')).toMatch(/abstract sound\(\): string/);
    });

    test('generic function: identity<T>(x: T): T', () => {
      const out = sigs(['function identity<T>(x: T): T { return x; }']);
      expect(out.join('\n')).toMatch(/function identity<T>\(x: T\): T/);
    });

    test('generic class: Container<T>', () => {
      const out = sigs([
        'class Container<T> {',
        '  value: T;',
        '}',
      ]);
      expect(out.join('\n')).toMatch(/class Container<T>/);
    });

    test('class method with TS return type annotation', () => {
      const out = sigs([
        'class C {',
        '  foo(x: number): string { return String(x); }',
        '}',
      ]);
      expect(out.join('\n')).toMatch(/foo\(x: number\): string/);
    });

    test('TSX function component: () => JSX.Element', () => {
      const out = sigs([
        'function App(): JSX.Element {',
        '  return <div />;',
        '}',
      ]);
      expect(out.join('\n')).toMatch(/function App\(\): JSX\.Element/);
    });

    test('ESM re-export: export { foo } from "./bar"', () => {
      const out = sigs([`export { foo } from './bar';`]);
      expect(out.join('\n')).toMatch(/export \{ foo \} from/);
    });

    test('ESM aliased re-export: export { foo as bar } from "./baz"', () => {
      const out = sigs([`export { foo as bar } from './baz';`]);
      expect(out.join('\n')).toMatch(/export \{ foo as bar \} from/);
    });

    test('decorator above class — class is still captured (decorator dropped is acceptable)', () => {
      const out = sigs([
        '@Component({ selector: "x" })',
        'class Foo {}',
      ]);
      expect(out.join('\n')).toMatch(/class Foo/);
    });

    test('literal-union return type', () => {
      const out = sigs([`function getKind(): "a" | "b" { return "a"; }`]);
      expect(out.join('\n')).toMatch(/function getKind\(\):/);
    });

    test('export const generic arrow: <T>(x: T): T => x', () => {
      const out = sigs(['export const fn = <T>(x: T): T => x;']);
      expect(out.join('\n')).toMatch(/export const fn/);
    });

    // ── Language detection on all declared TS/TSX/MJS/CJS extensions ──

    test('_langOf maps .ts / .tsx / .mjs / .cjs / .jsx → "js"', () => {
      expect(tool._langOf('a.ts')).toBe('js');
      expect(tool._langOf('a.tsx')).toBe('js');
      expect(tool._langOf('a.mjs')).toBe('js');
      expect(tool._langOf('a.cjs')).toBe('js');
      expect(tool._langOf('a.jsx')).toBe('js');
      // Case-insensitive on the extension.
      expect(tool._langOf('a.TS')).toBe('js');
    });

    // ── Known gaps. These tests pin CURRENT (limited) behavior so an
    //    improvement to the parser fails them — at which point you
    //    update the assertion. Each gap is real and worth fixing in a
    //    tree-sitter migration. ─────────────────────────────────────

    describe('KNOWN GAPS — pin current limitations', () => {
      test('GAP: bare `interface` (no `export`) is NOT captured', () => {
        const out = sigs(['interface User { id: string; name: string; }']);
        expect(out).toEqual([]);
      });

      test('GAP: bare `type` alias (no `export`) is NOT captured', () => {
        const out = sigs(['type ID = string | number;']);
        expect(out).toEqual([]);
      });

      test('GAP: bare `enum` (no `export`) is NOT captured', () => {
        const out = sigs(['enum Color { Red, Green, Blue }']);
        expect(out).toEqual([]);
      });

      test('GAP: bare multi-line `interface` (no `export`) is NOT captured', () => {
        const out = sigs([
          'interface User {',
          '  id: string;',
          '}',
        ]);
        expect(out).toEqual([]);
      });

      test('GAP: typed arrow component `const App: React.FC = () => <div />` is NOT captured', () => {
        // The `: React.FC` annotation between the identifier and `=`
        // breaks the parser's "ident = arrow" recognition. Common in
        // older React+TS codebases.
        const out = sigs(['const App: React.FC = () => <div />;']);
        expect(out).toEqual([]);
      });

      test('GAP: async generator `async function* foo()` is NOT captured', () => {
        const out = sigs([
          'async function* stream(): AsyncIterableIterator<number> {',
          '  yield 1;',
          '}',
        ]);
        expect(out).toEqual([]);
      });

      test('GAP: destructured-arg arrow with type annotation is NOT captured', () => {
        // `const fn = ({ name }: { name: string }): string => …`
        // The destructured + typed parameter list trips the
        // ident = arrow recognition.
        const out = sigs([`const greet = ({ name }: { name: string }): string => \`hi \${name}\`;`]);
        expect(out).toEqual([]);
      });
    });
  });

  // ─────────────────────────────────────────────────────────────────
  // C / C++ — _parseC. Same approach as the JS path; we lock the
  // patterns the regex needs to handle on real-world C/CPP files so
  // a future refactor (e.g. swapping for tree-sitter) has explicit
  // behavior to preserve.
  // ─────────────────────────────────────────────────────────────────
  describe('_parseC — C/C++ skeleton extraction', () => {
    const opts = { publicOnly: false, withComments: false, includeImports: true };
    const sig = (lines) => tool._parseC(lines, opts).filter(e => e.kind === 'signature').map(e => e.text.trim());
    const imp = (lines) => tool._parseC(lines, opts).filter(e => e.kind === 'import').map(e => e.text.trim());

    test('#include with angle brackets and quotes', () => {
      const lines = [
        '#include <stdio.h>',
        '#include "myheader.h"',
      ];
      expect(imp(lines)).toEqual([
        '#include <stdio.h>',
        '#include "myheader.h"',
      ]);
    });

    test('#define captured as signature', () => {
      expect(sig(['#define MAX_BUF 1024', '#define PI 3.14'])).toEqual([
        '#define MAX_BUF 1024',
        '#define PI 3.14',
      ]);
    });

    test('class / struct / union / enum / namespace headers', () => {
      const lines = [
        'class Foo {',
        '};',
        'struct Bar {',
        '};',
        'union Baz { int a; };',
        'enum class Color { RED, GREEN };',
        'namespace loxia {',
        '}',
      ];
      const found = sig(lines).join('\n');
      expect(found).toMatch(/class Foo/);
      expect(found).toMatch(/struct Bar/);
      expect(found).toMatch(/union Baz/);
      expect(found).toMatch(/enum class Color/);
      expect(found).toMatch(/namespace loxia/);
    });

    test('typedef and using alias', () => {
      const lines = [
        'typedef unsigned long u64;',
        'using IntList = std::vector<int>;',
      ];
      const found = sig(lines).join('\n');
      expect(found).toMatch(/typedef unsigned long u64/);
      expect(found).toMatch(/using IntList/);
    });

    test('free function definition with simple return type', () => {
      const lines = [
        'int add(int a, int b) {',
        '  return a + b;',
        '}',
      ];
      expect(sig(lines).join('\n')).toMatch(/int add\(int a, int b\)/);
    });

    test('static + inline + const-qualified function definitions', () => {
      const lines = [
        'static inline int square(int x) { return x * x; }',
        'const char* greet(const char* name) {',
        '  return name;',
        '}',
      ];
      const found = sig(lines).join('\n');
      expect(found).toMatch(/static inline int square/);
      expect(found).toMatch(/const char\* greet/);
    });

    test('method, constructor, and destructor', () => {
      const lines = [
        'Foo::Foo(int x) : x_(x) {',
        '}',
        'Foo::~Foo() {',
        '}',
        'int Foo::compute(int n) const {',
        '  return n * x_;',
        '}',
      ];
      const found = sig(lines).join('\n');
      expect(found).toMatch(/Foo::Foo\(int x\)/);
      expect(found).toMatch(/Foo::~Foo\(\)/);
      expect(found).toMatch(/int Foo::compute/);
    });

    test('REGRESSION: function CALLS are NOT misidentified as definitions', () => {
      // Without the call-rejection heuristic, the regex would happily
      // grab `printf("hi");` as a signature. Lock that it doesn't.
      const lines = [
        'int main(void) {',
        '  printf("hi");',
        '  foo();',
        '  return 0;',
        '}',
      ];
      // Only main(void) should be a signature; printf/foo are calls.
      const found = sig(lines);
      expect(found).toEqual(['int main(void) {']);
    });

    test('REGRESSION: a tiny realistic C file produces a useful skeleton', () => {
      const lines = [
        '#include <stdio.h>',
        '',
        '#define BUF_SIZE 256',
        '',
        'typedef struct {',
        '  int x;',
        '  int y;',
        '} Point;',
        '',
        'static int distance_sq(Point p) {',
        '  return p.x * p.x + p.y * p.y;',
        '}',
        '',
        'int main(int argc, char** argv) {',
        '  Point p = { 3, 4 };',
        '  printf("d² = %d\\n", distance_sq(p));',
        '  return 0;',
        '}',
      ];
      const all = tool._parseC(lines, opts);
      // Includes count as imports; the rest are signatures.
      const imports = all.filter(e => e.kind === 'import').map(e => e.text.trim());
      const signatures = all.filter(e => e.kind === 'signature').map(e => e.text.trim());
      expect(imports).toEqual(['#include <stdio.h>']);
      expect(signatures).toEqual(expect.arrayContaining([
        '#define BUF_SIZE 256',
        'static int distance_sq(Point p) {',
        'int main(int argc, char** argv) {',
      ]));
    });
  });

  test('_langOf detects C/C++ files', () => {
    expect(tool._langOf('foo.c')).toBe('c');
    expect(tool._langOf('foo.h')).toBe('c');
    expect(tool._langOf('foo.cpp')).toBe('c');
    expect(tool._langOf('foo.cxx')).toBe('c');
    expect(tool._langOf('foo.hpp')).toBe('c');
    expect(tool._langOf('foo.cc')).toBe('c');
    expect(tool._langOf('app.js')).toBe('js'); // sanity
    expect(tool._langOf('script.py')).toBe('python'); // sanity
  });

  test('execute skeleton on unsupported file type throws', async () => {
    mockFsPromises.stat.mockResolvedValue({
      isFile: () => true,
      isDirectory: () => false
    });

    await expect(tool.execute(
      { action: 'skeleton', path: 'data.json' },
      context
    )).rejects.toThrow('Unsupported file type');
  });

  // ─────────────────────────────────────────────────────────────────
  // Regex-free parser regressions — these cases are exactly the
  // kind of pattern that brittle regex would silently mis-classify.
  // They lock down the new string-utility implementation against
  // the most common false-positive risks.
  // ─────────────────────────────────────────────────────────────────
  describe('REGRESSION: no-regex parsers — false-positive guards', () => {
    const opts = { publicOnly: false, withComments: false, includeImports: true };

    // JS
    test('JS: `class` keyword does NOT match `classroom = …`', () => {
      const entries = tool._parseJS(['classroom = 1;'], opts);
      // No signature should be emitted — `classroom` is a plain var, not a class.
      expect(entries.filter(e => e.kind === 'signature').length).toBe(0);
    });
    test('JS: string literal containing `class` does NOT push a class frame', () => {
      // Pre-fix the original regex `(stripped.match(/\{/g) || []).length` could
      // be tripped by content inside strings if not pre-masked. The no-regex
      // implementation masks strings before counting braces.
      const entries = tool._parseJS([
        'const x = "class Foo {";',
      ], opts);
      // Only the `const` line should produce something; no class signature.
      const sigs = entries.filter(e => e.kind === 'signature').map(e => e.text);
      expect(sigs.some(s => s.startsWith('class '))).toBe(false);
    });
    test('JS: `// foo` and `/* foo */` line/block comments still suppress signatures', () => {
      const entries = tool._parseJS([
        '// export function fake() {}',
        '/* export function alsoFake() {} */',
        'export function real() {}',
      ], opts);
      const sigs = entries.filter(e => e.kind === 'signature').map(e => e.text.trim());
      expect(sigs).toEqual(['export function real() {}']);
    });
    test('JS: `require` as a substring inside another identifier is NOT an import', () => {
      // E.g. `prerequires = []` or `xrequireY()` — neither should be classified
      // as an import line by the require-call detector.
      const entries = tool._parseJS([
        'const xrequireY = makeIt();',
      ], opts);
      expect(entries.filter(e => e.kind === 'import').length).toBe(0);
    });
    test('JS: `==` after a global LHS does NOT trigger an assignment match', () => {
      const entries = tool._parseJS([
        'window.foo == bar;',
      ], opts);
      expect(entries.filter(e => e.kind === 'signature').length).toBe(0);
    });

    // C
    test('C: `class` inside a string literal does NOT push a class signature', () => {
      const entries = tool._parseC([
        'const char* msg = "class is a keyword in C++";',
      ], opts);
      // The C parser is line-based and doesn't deeply track string state;
      // this asserts the realistic case where the assignment isn't a class.
      // The line shouldn't surface as a "class …" signature.
      const sigs = entries.filter(e => e.kind === 'signature').map(e => e.text);
      expect(sigs.some(s => s.trim().startsWith('class '))).toBe(false);
    });
    test('C: bare function CALL `foo(x, y);` is NOT a definition', () => {
      const entries = tool._parseC(['  foo(1, 2);'], opts);
      expect(entries.filter(e => e.kind === 'signature').length).toBe(0);
    });

    // Python
    test('Python: `class` keyword does NOT match `classroom = 1`', () => {
      const entries = tool._parsePython(['classroom = 1'], opts);
      // It's a top-level data assignment, NOT a class signature.
      const sigs = entries.filter(e => e.kind === 'signature').map(e => e.text);
      expect(sigs.length).toBe(0);
      const datas = entries.filter(e => e.kind === 'data');
      expect(datas.length).toBe(1);
    });
    test('Python: `def` keyword on a line where it is NOT the leading token (e.g. `nodef(x):`) is not a function', () => {
      // The word-boundary check in startsWithKeyword catches this case.
      const entries = tool._parsePython(['nodef(x):'], opts);
      expect(entries.filter(e => e.kind === 'signature').length).toBe(0);
    });
    test('Python: relative import `from . import foo` is captured', () => {
      const entries = tool._parsePython(['from . import foo'], opts);
      expect(entries.filter(e => e.kind === 'import').length).toBe(1);
    });
    test('Python: docstring closes correctly on a one-liner', () => {
      const entries = tool._parsePython([
        'def hello():',
        '    """One-liner docstring."""',
        '    return 1',
      ], { ...opts, withComments: true });
      const kinds = entries.map(e => e.kind);
      expect(kinds).toContain('signature');
      expect(kinds).toContain('comment');
    });
  });
});
