import { test } from 'node:test';
import assert from 'node:assert/strict';
import { diffWords, DiffSegment } from './textDiff';

// Helper: reconstruct the old / new text from segments to prove correctness.
const oldText = (segs: DiffSegment[]) =>
  segs
    .filter((s) => s.type !== 'added')
    .map((s) => s.text)
    .join('');
const newText = (segs: DiffSegment[]) =>
  segs
    .filter((s) => s.type !== 'removed')
    .map((s) => s.text)
    .join('');

test('identical text yields a single equal segment', () => {
  const out = diffWords('hello world', 'hello world');
  assert.deepEqual(out, [{ type: 'equal', text: 'hello world' }]);
});

test('pure insertion marks only the new words as added', () => {
  const out = diffWords('hello world', 'hello there world');
  assert.equal(oldText(out), 'hello world');
  assert.equal(newText(out), 'hello there world');
  assert.deepEqual(
    out.filter((s) => s.type === 'added').map((s) => s.text.trim()),
    ['there'],
  );
  assert.equal(
    out.some((s) => s.type === 'removed'),
    false,
  );
});

test('pure deletion marks only the dropped words as removed', () => {
  const out = diffWords('hello there world', 'hello world');
  assert.equal(
    out.some((s) => s.type === 'added'),
    false,
  );
  assert.deepEqual(
    out.filter((s) => s.type === 'removed').map((s) => s.text.trim()),
    ['there'],
  );
});

test('a word replacement is a removed run followed by an added run', () => {
  const out = diffWords('hello world', 'hi world');
  // Reconstructs both sides.
  assert.equal(oldText(out), 'hello world');
  assert.equal(newText(out), 'hi world');
  const removed = out.filter((s) => s.type === 'removed').map((s) => s.text.trim());
  const added = out.filter((s) => s.type === 'added').map((s) => s.text.trim());
  assert.deepEqual(removed, ['hello']);
  assert.deepEqual(added, ['hi']);
});

test('whitespace and newlines are preserved in reconstruction', () => {
  const a = '  line one\nline two  ';
  const b = '  line one\nline three  ';
  const out = diffWords(a, b);
  assert.equal(oldText(out), a);
  assert.equal(newText(out), b);
});

test('empty <-> non-empty', () => {
  assert.deepEqual(diffWords('', 'new text'), [{ type: 'added', text: 'new text' }]);
  assert.deepEqual(diffWords('old text', ''), [{ type: 'removed', text: 'old text' }]);
  assert.deepEqual(diffWords('', ''), []);
});

test('diffWords does not mutate its inputs', () => {
  const a = 'alpha beta';
  const b = 'alpha gamma';
  diffWords(a, b);
  assert.equal(a, 'alpha beta');
  assert.equal(b, 'alpha gamma');
});

test('very large inputs fall back to a coarse whole-block diff', () => {
  const big = Array.from({ length: 5000 }, (_, i) => `w${i}`).join(' ');
  const out = diffWords(big, `${big} extra`);
  // Coarse fallback: one removed block + one added block (no per-word LCS).
  assert.deepEqual(
    out.map((s) => s.type),
    ['removed', 'added'],
  );
  assert.equal(oldText(out), big);
  assert.equal(newText(out), `${big} extra`);
});

test('word-level granularity (not character-level)', () => {
  const out = diffWords('cat', 'cats');
  // "cat" and "cats" are different tokens → full removed + added, no partial.
  assert.deepEqual(
    out.map((s) => s.type),
    ['removed', 'added'],
  );
});
