import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
  bytesToUrlBase64,
  devicePusherRequest,
  parsePush,
  parseResolved,
  parseStoredPush,
  roomFromTag,
  roomTarget,
  shouldSilence,
  stalePushers,
  syncAction,
  urlBase64ToBytes
} from '../src/lib/push-model.ts';

const ORIGIN = 'https://chat.example';

const FULL = {
  web_push: 8030,
  notification: {
    title: 'Ada in General',
    body: 'hello',
    navigate: `${ORIGIN}/?room=%21r%3Ax`,
    tag: 'room:!r:x',
    silent: false,
    app_badge: '4'
  },
  malice: { room_id: '!r:x', event_id: '$e', encrypted: true, mention: true, unread: 7 }
};

test('parsePush reads the full contract', () => {
  const d = parsePush(JSON.stringify(FULL), ORIGIN);
  assert.equal(d.title, 'Ada in General');
  assert.equal(d.body, 'hello');
  assert.equal(d.tag, 'room:!r:x');
  assert.equal(d.navigate, `${ORIGIN}/?room=%21r%3Ax`);
  assert.equal(d.badge, 4);
  assert.equal(d.roomId, '!r:x');
  assert.equal(d.eventId, '$e');
  assert.equal(d.mention, true);
  assert.equal(d.encrypted, true);
});

test('parsePush falls back for garbage, empty and missing data', () => {
  for (const raw of [null, '', 'not json', '[]', '"str"', '{}', 42]) {
    const d = parsePush(raw, ORIGIN);
    assert.equal(d.title, 'New message');
    assert.equal(d.body, '');
    assert.equal(d.tag, 'malice');
    assert.equal(d.roomId, null);
    assert.equal(d.badge, null);
  }
});

test('parsePush derives the tag and badge from the malice block', () => {
  const d = parsePush({ malice: { room_id: '!a:b', unread: 3 } }, ORIGIN);
  assert.equal(d.tag, 'room:!a:b');
  assert.equal(d.badge, 3);
  assert.equal(parsePush({ notification: { app_badge: '0' } }, ORIGIN).badge, 0);
  assert.equal(parsePush({ notification: { app_badge: '-2' } }, ORIGIN).badge, null);
});

test('parsePush drops navigate urls from other origins', () => {
  const d = parsePush({ notification: { navigate: 'https://evil.example/?room=x' } }, ORIGIN);
  assert.equal(d.navigate, null);
  assert.equal(parsePush({ notification: { navigate: '/?room=x' } }, ORIGIN).navigate, `${ORIGIN}/?room=x`);
});

test('roomTarget prefers navigate then the room then the root', () => {
  assert.equal(roomTarget('!r:x', `${ORIGIN}/?room=y`), `${ORIGIN}/?room=y`);
  assert.equal(roomTarget('!r:x', null), '/?room=!r%3Ax');
  assert.equal(roomTarget(null, null), '/');
  assert.equal(roomFromTag('room:!r:x'), '!r:x');
  assert.equal(roomFromTag('malice'), null);
});

test('shouldSilence keeps the banner but mutes it for a focused window or a silent payload', () => {
  const loud = parsePush(FULL, ORIGIN);
  assert.equal(shouldSilence(loud, false), false);
  assert.equal(shouldSilence(loud, true), true);
  assert.equal(shouldSilence({ ...loud, silent: true }, false), true);
});

test('parseResolved only replaces when the content differs', () => {
  const d = parsePush(FULL, ORIGIN);
  assert.deepEqual(parseResolved({ title: 'Ada', body: 'real text' }, d), { title: 'Ada', body: 'real text' });
  assert.deepEqual(parseResolved({ body: 'real text' }, d), { title: d.title, body: 'real text' });
  assert.equal(parseResolved({ title: d.title, body: d.body }, d), null);
  assert.equal(parseResolved(null, d), null);
  assert.equal(parseResolved({}, d), null);
});

test('syncAction repairs a lost or rotated subscription', () => {
  const stored = { endpoint: 'https://push/a', pushkey: 'k' };
  assert.equal(syncAction(false, null, stored), 'none');
  assert.equal(syncAction(true, null, stored), 'subscribe');
  assert.equal(syncAction(true, 'https://push/b', stored), 'register');
  assert.equal(syncAction(true, 'https://push/a', null), 'register');
  assert.equal(syncAction(true, 'https://push/a', stored), 'none');
});

test('base64url round trips without padding', () => {
  const bytes = new Uint8Array([251, 255, 190, 1, 2]);
  const text = bytesToUrlBase64(bytes);
  assert.equal(text, '-_--AQI');
  assert.deepEqual([...urlBase64ToBytes(text)], [...bytes]);
});

test('parseStoredPush tolerates junk', () => {
  assert.equal(parseStoredPush(null), null);
  assert.equal(parseStoredPush('{'), null);
  assert.deepEqual(parseStoredPush('{"endpoint":"e","pushkey":"k"}'), { endpoint: 'e', pushkey: 'k' });
  assert.deepEqual(parseStoredPush('{"endpoint":"e"}'), { endpoint: 'e', pushkey: null });
});

test('devicePusherRequest targets the same-origin gateway', () => {
  const r = devicePusherRequest({ endpoint: 'https://push/a', p256dh: 'pk', auth: 'au' }, ORIGIN, 'en', 'b1');
  assert.equal(r.op, 'set_pusher');
  assert.equal(r.app_id, 'dev.malice.device');
  assert.equal(r.pushkey, 'pk');
  assert.equal(r.url, `${ORIGIN}/_matrix/push/v1/notify`);
  assert.equal(r.format, 'Full');
  assert.deepEqual(r.data, { endpoint: 'https://push/a', auth: 'au', browser: 'b1' });
});

test('stalePushers only picks this browser own old endpoints', () => {
  const mk = (pushkey: string, endpoint: string, browser: string, app_id = 'dev.malice.device') => ({ app_id, pushkey, data: { endpoint, browser } });
  const reply = { pushers: [mk('a', 'old', 'b1'), mk('b', 'cur', 'b1'), mk('c', 'old', 'b2'), mk('d', 'old', 'b1', 'other.app')] };
  assert.deepEqual(stalePushers(reply, 'b1', 'cur'), ['a']);
  assert.deepEqual(stalePushers(null, 'b1', 'cur'), []);
});
