import assert from 'node:assert/strict';
import { test } from 'node:test';
import {
  arrivalIn,
  badgeTotal,
  isRoomWatched,
  notifyingCount,
  planNotification,
  sourceFromTimeline,
  type NotificationSource
} from '../src/lib/notification-model.ts';
import type { RoomSummary, TimelineItem } from '../src/lib/types.ts';

const NOW = 1_000_000_000;

function room(extra: Partial<RoomSummary> = {}): RoomSummary {
  return {
    room_id: '!r:x',
    name: 'General',
    membership: 'Joined',
    unread_messages: 0,
    unread_mentions: 0,
    unread_notifications: 0,
    space: false,
    direct: false,
    notification_mode: null,
    latest_event_timestamp_ms: NOW - 1000,
    avatar: 'mxc://x/room',
    ...extra
  } as RoomSummary;
}

function source(extra: Partial<NotificationSource> = {}): NotificationSource {
  return { senderId: '@bob:x', senderName: 'Bob', senderAvatar: 'mxc://x/bob', kind: 'Text', body: 'hi', undecryptable: false, highlight: false, ...extra };
}

test('counts only notifying unread per mode', () => {
  assert.equal(notifyingCount(room({ unread_notifications: 0, unread_messages: 9 })), 9);
  assert.equal(notifyingCount(room({ direct: true, unread_messages: 2, unread_mentions: 1 })), 3);
  assert.equal(notifyingCount(room({ notification_mode: 'Mute', unread_mentions: 4 })), 0);
  assert.equal(notifyingCount(room({ notification_mode: 'MentionsAndKeywordsOnly', unread_notifications: 5, unread_mentions: 1 })), 1);
  assert.equal(notifyingCount(room({ space: true, unread_mentions: 1 })), 0);
  assert.equal(notifyingCount(room({ membership: 'Invited', unread_mentions: 1 })), 0);
});

test('badge sums notifying counts across rooms', () => {
  const rooms = [room({ unread_messages: 2 }), room({ direct: true, unread_messages: 1 }), room({ notification_mode: 'Mute', unread_messages: 9 })];
  assert.equal(badgeTotal(rooms), 3);
  assert.equal(badgeTotal([]), 0);
});

test('arrival needs a known previous room and a count increase', () => {
  const before = room({ unread_messages: 1 });
  assert.equal(arrivalIn(undefined, before, NOW), null);
  assert.equal(arrivalIn(before, before, NOW), null);
  assert.equal(arrivalIn(before, room({ unread_messages: 0 }), NOW), null);
  assert.deepEqual(arrivalIn(before, room({ unread_messages: 2 }), NOW), { mention: false });
  assert.deepEqual(arrivalIn(before, room({ unread_messages: 2, unread_mentions: 1 }), NOW), { mention: true });
});

test('arrival ignores stale backfill', () => {
  const next = room({ unread_messages: 1, latest_event_timestamp_ms: NOW - 10 * 60 * 1000 });
  assert.equal(arrivalIn(room(), next, NOW), null);
});

test('only the focused open room is watched', () => {
  assert.equal(isRoomWatched('!a', '!a', true, true), true);
  assert.equal(isRoomWatched('!a', '!a', false, true), false);
  assert.equal(isRoomWatched('!a', '!a', true, false), false);
  assert.equal(isRoomWatched('!a', '!b', true, true), false);
});

test('group notification prefixes the sender and tags per room', () => {
  const plan = planNotification(room(), source(), { mention: false });
  assert.equal(plan.title, 'General');
  assert.equal(plan.body, 'Bob: hi');
  assert.equal(plan.tag, 'room:!r:x');
  assert.equal(plan.renotify, false);
  assert.equal(plan.iconMxc, 'mxc://x/room');
});

test('direct notification titles the sender and uses their avatar', () => {
  const plan = planNotification(room({ direct: true, name: 'Bob' }), source(), { mention: false });
  assert.equal(plan.title, 'Bob');
  assert.equal(plan.body, 'hi');
  assert.equal(plan.iconMxc, 'mxc://x/bob');
});

test('mentions and keyword highlights renotify', () => {
  assert.equal(planNotification(room(), source(), { mention: true }).renotify, true);
  assert.equal(planNotification(room(), source({ highlight: true }), { mention: false }).renotify, true);
});

test('undecryptable and missing content get a generic body', () => {
  assert.equal(planNotification(room(), source({ undecryptable: true, body: null }), { mention: false }).body, 'Bob: New messages');
  assert.equal(planNotification(room({ direct: true }), null, { mention: false }).body, 'New message');
});

test('media and emotes get readable previews', () => {
  assert.equal(planNotification(room({ direct: true }), source({ kind: 'Image', body: 'cat.png' }), { mention: false }).body, 'Sent an image');
  assert.equal(planNotification(room(), source({ kind: 'Emote', body: 'waves' }), { mention: false }).body, 'Bob waves');
});

test('falls back to the user id local part when no display name is known', () => {
  assert.equal(planNotification(room(), source({ senderName: null }), { mention: false }).body, 'bob: hi');
});

test('timeline source skips own newest and non-message items', () => {
  const message = (own: boolean, body: string): TimelineItem =>
    ({
      kind: 'event',
      event: {
        own,
        sender: { user_id: '@bob:x', display_name: 'Bob', avatar: null, name_ambiguous: false },
        content: { kind: 'Message', message: { kind: 'Text', body } }
      }
    }) as TimelineItem;
  const membership = { kind: 'event', event: { own: false, sender: { user_id: '@c:x' }, content: { kind: 'RoomCreated' } } } as TimelineItem;
  assert.equal(sourceFromTimeline([message(false, 'a'), membership])?.body, 'a');
  assert.equal(sourceFromTimeline([message(false, 'a'), message(true, 'b')]), null);
  assert.equal(sourceFromTimeline(undefined), null);
});
