import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createStore } from 'jotai';
import { enableMapSet } from 'immer';
import {
  TYPING_TIMEOUT_MS,
  roomIdToTypingMembersAtom,
  type IRoomIdToTypingMembers,
} from './typingMembers';

// The add/remove/dedup/filter logic lives in the unexported putTypingMember /
// deleteTypingMember reducers, reachable only through `roomIdToTypingMembersAtom`.
// We exercise it via a jotai store (the pure reducer path). The PUT branch also
// schedules a real setTimeout for auto-expiry; that timer-driven cleanup and the
// React `useBindRoomIdToTypingMembersAtom` hook wiring are not covered here.
// The reducers `produce` over an immer-managed Map. The app turns this on once
// at startup (src/index.tsx calls enableMapSet()); we replicate that here.
enableMapSet();

const ROOM = '!room:server';
const A = '@alice:server';
const B = '@bob:server';

const members = (store: ReturnType<typeof createStore>): IRoomIdToTypingMembers =>
  store.get(roomIdToTypingMembersAtom);

const userIds = (store: ReturnType<typeof createStore>, roomId: string): string[] =>
  (members(store).get(roomId) ?? []).map((r) => r.userId);

test('TYPING_TIMEOUT_MS is 5 seconds', () => {
  assert.equal(TYPING_TIMEOUT_MS, 5000);
});

test('starts empty', () => {
  const store = createStore();
  assert.equal(members(store).size, 0);
});

test('PUT adds a typing member to a room', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
  assert.deepEqual(userIds(store, ROOM), [A]);
});

test('PUT dedups by userId, keeping the latest ts', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 99 });

  const receipts = members(store).get(ROOM) ?? [];
  assert.equal(receipts.length, 1);
  assert.equal(receipts[0].userId, A);
  assert.equal(receipts[0].ts, 99);
});

test('PUT keeps distinct users side by side', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });
  assert.deepEqual(userIds(store, ROOM).sort(), [A, B].sort());
});

test('typing members are scoped per room', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r1:s', userId: A, ts: 1 });
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r2:s', userId: B, ts: 1 });
  assert.deepEqual(userIds(store, '!r1:s'), [A]);
  assert.deepEqual(userIds(store, '!r2:s'), [B]);
});

test('DELETE removes a user and drops the room when it becomes empty', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });

  store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
  assert.deepEqual(userIds(store, ROOM), [B]);

  store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
  assert.equal(members(store).has(ROOM), false);
});

test('DELETE of an absent user is a no-op (no empty room created)', () => {
  const store = createStore();
  store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
  assert.equal(members(store).has(ROOM), false);

  store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
  store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
  assert.deepEqual(userIds(store, ROOM), [A]);
});
