import { test } from 'node:test';
import assert from 'node:assert/strict';
import { findAndReplace } from './findAndReplace';

// Helpers that record exactly what the callbacks receive.
const tagPart = (text: string, pushIndex: number) => ({ part: text, at: pushIndex });
const tagMatch = (match: RegExpExecArray | RegExpMatchArray, pushIndex: number) => ({
  match: match[0],
  at: pushIndex,
});

test('findAndReplace interleaves converted parts and replacements (global regex)', () => {
  const result = findAndReplace('a1b2c', /\d/g, tagMatch, tagPart);
  assert.deepEqual(result, [
    { part: 'a', at: 0 },
    { match: '1', at: 1 },
    { part: 'b', at: 2 },
    { match: '2', at: 3 },
    { part: 'c', at: 4 },
  ]);
});

test('findAndReplace handles a NON-global regex as a single match (regression: no infinite loop)', () => {
  // Before the fix, a non-global regex with a match looped forever (match was
  // only reassigned inside `if (regex.global)`), OOM-crashing the process.
  const result = findAndReplace('a1b2c', /\d/, tagMatch, tagPart);
  assert.deepEqual(result, [
    { part: 'a', at: 0 },
    { match: '1', at: 1 },
    // remainder after the first (only) match is a single converted part
    { part: 'b2c', at: 2 },
  ]);
});

test('findAndReplace pushIndex reflects result.length at push time', () => {
  // The indices above already assert this; here we double-check the trailing part.
  const result = findAndReplace('x9', /\d/g, tagMatch, tagPart);
  // 'x' at 0, '9' at 1, '' at 2
  assert.equal(result[result.length - 1].at, 2);
});

test('findAndReplace with no match returns the whole text as a single converted part', () => {
  const result = findAndReplace('hello', /\d/g, tagMatch, tagPart);
  assert.deepEqual(result, [{ part: 'hello', at: 0 }]);
});

test('findAndReplace on empty input returns a single empty converted part', () => {
  const result = findAndReplace('', /\d/g, tagMatch, tagPart);
  assert.deepEqual(result, [{ part: '', at: 0 }]);
});

test('findAndReplace emits empty parts between adjacent matches and at edges', () => {
  const result = findAndReplace('12', /\d/g, tagMatch, tagPart);
  assert.deepEqual(result, [
    { part: '', at: 0 },
    { match: '1', at: 1 },
    { part: '', at: 2 },
    { match: '2', at: 3 },
    { part: '', at: 4 },
  ]);
});

test('findAndReplace with a leading match emits an empty leading part', () => {
  const result = findAndReplace('1abc', /\d/g, tagMatch, tagPart);
  assert.deepEqual(result, [
    { part: '', at: 0 },
    { match: '1', at: 1 },
    { part: 'abc', at: 2 },
  ]);
});

// NOTE: A non-global regex is intentionally NOT tested here. With `regex.global`
// false, the source's `while (match !== null)` loop never reassigns `match`
// (reassignment is guarded by `if (regex.global)`), so it loops forever on any
// matching input. See the report. All real callers pass global regexes.

test('findAndReplace supports multi-character matches', () => {
  const result = findAndReplace('foo<<bar>>baz', /<<|>>/g, tagMatch, tagPart);
  assert.deepEqual(result, [
    { part: 'foo', at: 0 },
    { match: '<<', at: 1 },
    { part: 'bar', at: 2 },
    { match: '>>', at: 3 },
    { part: 'baz', at: 4 },
  ]);
});

test('findAndReplace can build a transformed string', () => {
  const out = findAndReplace<string, string>(
    'cat and dog',
    /cat|dog/g,
    (m) => (m[0] === 'cat' ? 'CAT' : 'DOG'),
    (text) => text,
  ).join('');
  assert.equal(out, 'CAT and DOG');
});

test('findAndReplace passes the full match object to replace', () => {
  const captured: Array<string | undefined> = [];
  findAndReplace(
    'name=value',
    /(\w+)=(\w+)/g,
    (m) => {
      captured.push(m[1], m[2]);
      return null;
    },
    () => null,
  );
  assert.deepEqual(captured, ['name', 'value']);
});
