// Run: node --test frontend/src/shared/interactiveRanking.test.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { rankAndCapInteractives, goalKeywords, type RankItem } from './interactiveRanking.ts';

const mk = (role: string, name: string, id = 0): RankItem => ({ role, name, backendNodeId: id });

test('consecutive twins with same role+name collapse', () => {
  const { shown } = rankAndCapInteractives([
    mk('button', 'Like', 1),
    mk('button', 'Like', 2),
    mk('button', 'Like', 3),
    mk('button', 'Share', 4),
  ]);
  assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 4]);
});

test('same role+name in different frames (sessionId) is NOT collapsed at the seam', () => {
  const root: RankItem = { role: 'button', name: 'Close', backendNodeId: 1 };
  const child: RankItem = { role: 'button', name: 'Close', backendNodeId: 2, sessionId: 'frameA' };
  const { shown } = rankAndCapInteractives([root, child]);
  // both survive: they are genuinely different elements across a frame boundary
  assert.equal(shown.length, 2);
  assert.ok(shown.some((x) => x.sessionId === 'frameA'));
});

test('non-consecutive same name is preserved (real list items)', () => {
  const { shown } = rankAndCapInteractives([
    mk('button', 'Add to cart', 1),
    mk('link', 'Widget A', 2),
    mk('button', 'Add to cart', 3),
    mk('link', 'Widget B', 4),
    mk('button', 'Add to cart', 5),
  ]);
  // all three "Add to cart" survive because they are not back-to-back
  const carts = shown.filter((x) => x.name === 'Add to cart');
  assert.equal(carts.length, 3);
});

test('display order is DOCUMENT order, not rank order (so ordinals can be counted)', () => {
  const { shown } = rankAndCapInteractives([
    mk('option', 'opt', 1),
    mk('checkbox', 'agree', 2),
    mk('button', 'Go', 3),
    mk('textbox', 'email', 4),
  ]);
  // all survive the cap; they render top-to-bottom as they appear on the page,
  // NOT re-sorted by role (which would scramble "the 4th thing")
  assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3, 4]);
});

test('rank still decides cap SURVIVAL: a high-priority input buried in options is kept', () => {
  const items = [
    ...Array.from({ length: 40 }, (_, i) => mk('option', `opt${i}`, i)),
    mk('textbox', 'email', 900),
    ...Array.from({ length: 40 }, (_, i) => mk('option', `optb${i}`, 100 + i)),
  ];
  const { shown } = rankAndCapInteractives(items, { cap: 30 });
  assert.ok(shown.some((x) => x.backendNodeId === 900), 'the input survives the cap by rank');
});

test('preserves document order within the same priority tier', () => {
  const { shown } = rankAndCapInteractives([
    mk('button', 'First', 1),
    mk('link', 'Second', 2),
    mk('button', 'Third', 3),
  ]);
  // button and link share tier 1; original order First, Second, Third holds
  assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3]);
});

test('caps to N and reports the truncated remainder', () => {
  const items = Array.from({ length: 150 }, (_, i) => mk('link', `L${i}`, i));
  const { shown, truncated } = rankAndCapInteractives(items, { cap: 60 });
  assert.equal(shown.length, 60);
  assert.equal(truncated, 90);
});

test('cap of 0 means no cap', () => {
  const items = Array.from({ length: 5 }, (_, i) => mk('link', `L${i}`, i));
  const { shown, truncated } = rankAndCapInteractives(items, { cap: 0 });
  assert.equal(shown.length, 5);
  assert.equal(truncated, 0);
});

test('goal-matched element survives the cap and displays IN PLACE (document order)', () => {
  const items = Array.from({ length: 100 }, (_, i) => mk('link', `Item ${i}`, i));
  items.push(mk('button', 'Checkout now', 999));  // last on the page
  const { shown } = rankAndCapInteractives(items, { cap: 30, goal: 'click checkout' });
  // it survives the cap (rank kept it)...
  assert.ok(shown.some((x) => x.backendNodeId === 999), 'checkout should be retained');
  // ...and renders at its real position (last), not floated to [0]
  assert.equal(shown[shown.length - 1].backendNodeId, 999);
});

test('display order stays document order with no goal', () => {
  const { shown } = rankAndCapInteractives([
    mk('option', 'opt', 1),
    mk('button', 'Go', 2),
    mk('textbox', 'email', 3),
  ]);
  assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3]);
});

test('goalKeywords strips stopwords, action verbs, and short tokens', () => {
  assert.deepEqual(goalKeywords('Click the Submit button to send'), ['submit', 'send']);
  assert.deepEqual(goalKeywords('type into the search box'), ['search']);
  assert.deepEqual(goalKeywords(''), []);
});

test('empty input yields empty result', () => {
  const { shown, truncated } = rankAndCapInteractives([]);
  assert.equal(shown.length, 0);
  assert.equal(truncated, 0);
});

test('unknown role is not dropped (survives the cap), displayed in document order', () => {
  const { shown } = rankAndCapInteractives([
    mk('option', 'opt', 1),
    mk('weirdrole', 'mystery', 2),
    mk('textbox', 'field', 3),
  ]);
  assert.deepEqual(shown.map((x) => x.backendNodeId), [1, 2, 3]);
  assert.ok(shown.some((x) => x.role === 'weirdrole'));
});

test('docOrder:false keeps legacy rank-order display (the A/B off-arm)', () => {
  const { shown } = rankAndCapInteractives([
    mk('option', 'opt', 1),
    mk('button', 'Go', 2),
    mk('textbox', 'email', 3),
  ], { docOrder: false });
  // rank order: textbox(0) < button(1) < option(3)
  assert.deepEqual(shown.map((x) => x.backendNodeId), [3, 2, 1]);
});
