/**
 * Tests for parserHelpers — the no-regex string primitives that replace
 * the regex-heavy classifiers in codeMapTool.js.
 *
 * Each helper has its own describe block. Edge-case coverage is the
 * whole point of this file — if any of these primitives is wrong, the
 * downstream parsers are silently wrong.
 *
 * NO REGEX in this file either (matches the no-regex policy of the
 * module under test).
 */
import { describe, test, expect } from '@jest/globals';
import {
  isWordChar,
  isIdentStart,
  isWhitespace,
  skipWhitespace,
  skipWhitespaceAndLineComment,
  readIdent,
  startsWithKeyword,
  consumeKeyword,
  consumeAnyKeyword,
  trimmedStartsWithAny,
  indexOfWord,
  containsWord,
  stripStringsAndLineComments,
  countChar,
  consumeDottedAssignTail,
  trimmedEndsWith,
} from '../parserHelpers.js';

describe('character predicates', () => {
  test('isWordChar: letters, digits, underscore', () => {
    for (const c of 'aZ0_9X') expect(isWordChar(c)).toBe(true);
    for (const c of ' \t-.+*/()[]{};:,?@#!=') expect(isWordChar(c)).toBe(false);
  });
  test('isWordChar: undefined / empty', () => {
    expect(isWordChar()).toBe(false);
    expect(isWordChar('')).toBe(false);
    expect(isWordChar(undefined)).toBe(false);
  });
  test('isIdentStart: letters + underscore, NOT digit', () => {
    expect(isIdentStart('a')).toBe(true);
    expect(isIdentStart('Z')).toBe(true);
    expect(isIdentStart('_')).toBe(true);
    expect(isIdentStart('0')).toBe(false);   // digit
    expect(isIdentStart('-')).toBe(false);
    expect(isIdentStart(undefined)).toBe(false);
  });
  test('isWhitespace: space + tab + vtab + ff, NOT newline', () => {
    expect(isWhitespace(' ')).toBe(true);
    expect(isWhitespace('\t')).toBe(true);
    expect(isWhitespace('\v')).toBe(true);
    expect(isWhitespace('\f')).toBe(true);
    expect(isWhitespace('\n')).toBe(false);
    expect(isWhitespace('a')).toBe(false);
  });
});

describe('skipWhitespace + skipWhitespaceAndLineComment', () => {
  test('skipWhitespace lands on first non-ws char', () => {
    expect(skipWhitespace('   abc', 0)).toBe(3);
    expect(skipWhitespace('\t  x',   0)).toBe(3);
    expect(skipWhitespace('no-leading')).toBe(0);
    expect(skipWhitespace('   ',     0)).toBe(3);   // all-whitespace
  });
  test('skipWhitespace honours idx start', () => {
    expect(skipWhitespace('xx   y', 2)).toBe(5);
  });
  test('skipWhitespaceAndLineComment: `//` jumps to end-of-line', () => {
    expect(skipWhitespaceAndLineComment('   // hello there')).toBe(17);
    expect(skipWhitespaceAndLineComment('x // y', 1)).toBe(6);   // skip the leading space then "// y"
    expect(skipWhitespaceAndLineComment('x')).toBe(0);            // no leading ws or comment
  });
});

describe('readIdent', () => {
  test('reads ident from start', () => {
    expect(readIdent('foo bar')).toEqual({ ident: 'foo', nextIdx: 3 });
  });
  test('underscores + digits inside an ident', () => {
    expect(readIdent('_foo123 ')).toEqual({ ident: '_foo123', nextIdx: 7 });
  });
  test('returns null when no ident at idx', () => {
    expect(readIdent('123abc')).toBeNull();    // starts with digit
    expect(readIdent('-abc')).toBeNull();
    expect(readIdent('  abc')).toBeNull();      // whitespace, not letter
  });
  test('honours idx start', () => {
    expect(readIdent('  foo', 2)).toEqual({ ident: 'foo', nextIdx: 5 });
  });
});

describe('startsWithKeyword + consumeKeyword + consumeAnyKeyword', () => {
  test('REGRESSION: word boundary — `class` does NOT match `classroom`', () => {
    expect(startsWithKeyword('classroom = 1', 'class')).toBe(false);
    expect(startsWithKeyword('class Foo {', 'class')).toBe(true);
  });
  test('end-of-string is a valid right boundary', () => {
    expect(startsWithKeyword('return', 'return')).toBe(true);
  });
  test('keyword followed by punctuation is OK', () => {
    expect(startsWithKeyword('foo();', 'foo')).toBe(true);
    expect(startsWithKeyword('foo.bar', 'foo')).toBe(true);
  });
  test('consumeKeyword returns idx past the keyword on hit; -1 on miss', () => {
    expect(consumeKeyword('export const x', 'export')).toBe(6);
    expect(consumeKeyword('exporting',       'export')).toBe(-1);  // word-boundary fail
    expect(consumeKeyword('  export', 'export', 2)).toBe(8);
  });
  test('consumeAnyKeyword tries each in order', () => {
    expect(consumeAnyKeyword('class Foo', ['function', 'class'])).toEqual({ keyword: 'class', nextIdx: 5 });
    expect(consumeAnyKeyword('classroom', ['class'])).toBeNull();
  });
});

describe('trimmedStartsWithAny', () => {
  test('matches the first keyword that fits, after leading whitespace', () => {
    expect(trimmedStartsWithAny('   export const x', ['export', 'class'])).toBe(true);
    expect(trimmedStartsWithAny('\tclass Foo {',     ['function', 'class'])).toBe(true);
    expect(trimmedStartsWithAny('   classroom',      ['class'])).toBe(false);
  });
});

describe('indexOfWord + containsWord', () => {
  test('word-boundary match in middle of string', () => {
    expect(indexOfWord('a require b', 'require')).toBe(2);
    expect(containsWord('  require()',  'require')).toBe(true);
  });
  test('REGRESSION: does not match within larger word', () => {
    expect(indexOfWord('aware', 'are')).toBe(-1);
    expect(indexOfWord('myFunctional', 'Function')).toBe(-1);
  });
  test('match at the very start / end of string', () => {
    expect(indexOfWord('return x', 'return')).toBe(0);
    expect(indexOfWord('x return', 'return')).toBe(2);
  });
  test('honours fromIdx', () => {
    expect(indexOfWord('require x require y', 'require', 1)).toBe(10);
  });
  test('empty needle returns -1 (no crash)', () => {
    expect(indexOfWord('foo', '')).toBe(-1);
  });
});

describe('stripStringsAndLineComments', () => {
  test('REGRESSION: single-quoted strings replaced by spaces, length preserved', () => {
    const out = stripStringsAndLineComments(`if ('foo' === bar) {`);
    expect(out.length).toBe(20);
    expect(out).toBe(`if ('   ' === bar) {`);
  });
  test('REGRESSION: double-quoted strings replaced', () => {
    const out = stripStringsAndLineComments(`var s = "class Foo {";`);
    // The brace-counting downstream depends on the `{` inside the string being masked.
    expect(out.includes('{')).toBe(false);
    expect(out.length).toBe(22);
  });
  test('REGRESSION: template literal mask preserves outer length', () => {
    const out = stripStringsAndLineComments('var s = `hi { ${ name } }`;');
    expect(out.length).toBe('var s = `hi { ${ name } }`;'.length);
    // The two `{`s inside the backtick literal must be masked.
    expect(out.includes('{')).toBe(false);
  });
  test('escapes inside strings: \\\' and \\"', () => {
    const input = `var s = 'a\\'b';`;   // r/aw: var s = 'a\'b';
    const out = stripStringsAndLineComments(input);
    expect(out.length).toBe(input.length);   // length preserved (key invariant)
    expect(out.includes('{')).toBe(false);
    // The open + close quote stay; the escape + escaped char + middle are masked.
    expect(out.startsWith(`var s = '`)).toBe(true);
    expect(out.endsWith(`';`)).toBe(true);
  });
  test('line comment from // to EOL replaced', () => {
    const input = 'var x = 1; // a = { open';
    const out = stripStringsAndLineComments(input);
    expect(out.length).toBe(input.length);    // length preserved
    expect(out.startsWith('var x = 1;')).toBe(true);
    // The `{` inside the comment is masked.
    expect(out.includes('{')).toBe(false);
    expect(out.includes('/')).toBe(false);    // // is gone too
  });
  test('unterminated string masks the rest', () => {
    const out = stripStringsAndLineComments(`var s = 'unfinished`);
    // Should not throw; the open quote is preserved + everything after is masked.
    expect(out.length).toBe(19);
  });
});

describe('countChar', () => {
  test('counts a single char', () => {
    expect(countChar('a{b{c}d}', '{')).toBe(2);
    expect(countChar('a{b{c}d}', '}')).toBe(2);
    expect(countChar('xxx', 'y')).toBe(0);
    expect(countChar('', '{')).toBe(0);
  });
});

describe('consumeDottedAssignTail', () => {
  test('returns idx past `=` after a dotted access', () => {
    const s = 'window.MyApp = function () {';
    // Caller has consumed `window`; idx is 6.
    const next = consumeDottedAssignTail(s, 6);
    expect(next).toBe(15);     // past `= `
    expect(s.slice(next)).toBe('function () {');
  });
  test('returns -1 for == / === (comparison, not assignment)', () => {
    expect(consumeDottedAssignTail('a == b', 1)).toBe(-1);
    expect(consumeDottedAssignTail('a === b', 1)).toBe(-1);
  });
  test('returns -1 when the LHS continues with junk before `=`', () => {
    expect(consumeDottedAssignTail('a.b.c', 1)).toBe(-1);
    expect(consumeDottedAssignTail('a.0',   1)).toBe(-1);  // .digit isn't an ident
  });
  test('handles multi-level dotted property accesses', () => {
    const s = 'a.b.c.d = 1';
    const next = consumeDottedAssignTail(s, 1);
    expect(next).toBe(10);
    expect(s.slice(next)).toBe('1');
  });
});

describe('trimmedEndsWith', () => {
  test('matches with trailing whitespace + optional semicolon', () => {
    expect(trimmedEndsWith('int x(int n) {',   '{')).toBe(true);
    expect(trimmedEndsWith('int x(int n) {  ', '{')).toBe(true);
    expect(trimmedEndsWith('return 1;',        ';')).toBe(true);
    expect(trimmedEndsWith('return 1; ',       '1')).toBe(true);  // ; allowed at end
  });
  test('REGRESSION: does not match when not at the trimmed end', () => {
    expect(trimmedEndsWith('a { b ',  'a')).toBe(false);
    expect(trimmedEndsWith('foo',     'x')).toBe(false);
  });
});
