import assert from 'node:assert/strict';
import { test } from 'node:test';
import { newShareCache, parseShared } from '../src/lib/sharing.ts';
import { observeArrivals } from '../src/lib/arrivals.ts';
import type { TimelineEvent, TimelineItem } from '../src/lib/types.ts';

interface Row {
  id: string;
  count: number;
}

function parse(entry: unknown): Row | null {
  const raw = entry as Record<string, unknown>;
  return typeof raw['id'] === 'string' ? { id: raw['id'], count: Number(raw['count']) } : null;
}

test('unchanged entries keep their object and unchanged lists keep their array', () => {
  const cache = newShareCache<Row>();
  const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse);
  const second = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse);
  assert.equal(second, first);
});

test('a changed entry gets a new object while its neighbours keep theirs', () => {
  const cache = newShareCache<Row>();
  const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse);
  const second = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 2 }], parse);
  assert.notEqual(second, first);
  assert.equal(second[0], first[0]);
  assert.notEqual(second[1], first[1]);
  assert.equal(second[1].count, 2);
});

test('appended and removed entries change the array', () => {
  const cache = newShareCache<Row>();
  const first = parseShared(cache, [{ id: 'a', count: 1 }], parse);
  const grown = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse);
  assert.notEqual(grown, first);
  assert.equal(grown[0], first[0]);
  const shrunk = parseShared(cache, [{ id: 'b', count: 1 }], parse);
  assert.equal(shrunk[0], grown[1]);
});

test('unparsable entries are skipped and non-arrays yield an empty list', () => {
  const cache = newShareCache<Row>();
  assert.equal(parseShared(cache, [{ id: 'a', count: 1 }, { nope: true }], parse).length, 1);
  assert.deepEqual(parseShared(cache, 'x', parse), []);
});

function event(id: string, timestamp_ms: number): TimelineItem {
  return { kind: 'event', event: { item_id: id, timestamp_ms, own: false } as TimelineEvent };
}

test('arrivals keeps array and lagged item identity across identical updates', () => {
  const MINUTE = 60 * 1000;
  const known = event('a', 90 * MINUTE);
  const late = event('late', 80 * MINUTE);
  const first = observeArrivals([known], [known, late], 0, 100 * MINUTE);
  assert.ok(first);
  const second = observeArrivals(first, [known, late], 0, 101 * MINUTE);
  assert.equal(second, first);
});

test('identified entries are matched by identity and evicted when absent', () => {
  const cache = newShareCache<Row>();
  const identify = (entry: unknown) => (entry as Row).id;
  const first = parseShared(cache, [{ id: 'a', count: 1 }, { id: 'b', count: 1 }], parse, identify);
  const second = parseShared(cache, [{ id: 'b', count: 1 }, { id: 'c', count: 1 }], parse, identify);
  assert.equal(second[0], first[1]);
  assert.deepEqual([...cache.byKey.keys()], ['b', 'c']);
});

test('a changed identified entry is reparsed under the same key', () => {
  const cache = newShareCache<Row>();
  const identify = (entry: unknown) => (entry as Row).id;
  const first = parseShared(cache, [{ id: 'a', count: 1 }], parse, identify);
  const second = parseShared(cache, [{ id: 'a', count: 2 }], parse, identify);
  assert.notEqual(second[0], first[0]);
  assert.equal(second[0].count, 2);
});
