import { test } from 'node:test';
import assert from 'node:assert/strict';
import type { Contact, Conversation } from '../../types';
import { normalizeHandle, rankConversationsByQuery } from './rankConversations.ts';

const ts = '2026-04-25T00:00:00Z';

function conv(overrides: Partial<Conversation> & Pick<Conversation, 'id'>): Conversation {
  return {
    id: overrides.id,
    deviceId: 'dev-1',
    externalId: overrides.externalId ?? `ext-${overrides.id}`,
    displayName: overrides.displayName ?? '',
    isGroup: overrides.isGroup ?? false,
    participants: overrides.participants ?? [],
    lastMessageAt: overrides.lastMessageAt ?? ts,
    messageCount: overrides.messageCount ?? 1,
    syncedAt: overrides.syncedAt ?? ts,
  };
}

function contact(overrides: Partial<Contact> & Pick<Contact, 'id' | 'displayName'>): Contact {
  return {
    id: overrides.id,
    deviceId: 'dev-1',
    handle: overrides.handle ?? '',
    displayName: overrides.displayName,
    phoneNumbers: overrides.phoneNumbers ?? [],
    emails: overrides.emails ?? [],
    syncedAt: overrides.syncedAt ?? ts,
    updatedAt: overrides.updatedAt ?? ts,
    summaryData: overrides.summaryData ?? null,
    summaryGeneratedAt: overrides.summaryGeneratedAt ?? null,
  };
}

test('normalizeHandle reduces phone formats to digits and keeps emails literal', () => {
  assert.equal(normalizeHandle('+1 (555) 123-4567'), '15551234567');
  assert.equal(normalizeHandle('555-123-4567'), '5551234567');
  assert.equal(normalizeHandle('  +15551234567  '), '15551234567');
  assert.equal(normalizeHandle('Shannon@example.COM'), 'shannon@example.com');
});

test('returns nothing for queries shorter than two chars', () => {
  const result = rankConversationsByQuery(
    [conv({ id: 'a', displayName: 'Shannon' })],
    [],
    's',
  );
  assert.deepEqual(result, []);
});

test('1:1 chat with empty displayName is matched via Contact phoneNumber (regression)', () => {
  // Reproduces the bug: Shannon's 1:1 thread has empty displayName and a raw
  // phone handle. Without contact enrichment, "shannon" wouldn't match.
  const conversations: Conversation[] = [
    // Some random earlier conversation whose body mentioned "shannon"
    conv({ id: 'noise', displayName: '', participants: ['+15559999999'] }),
    // Shannon's actual thread
    conv({ id: 'shannon', displayName: '', participants: ['+1 (555) 123-4567'] }),
  ];
  const contacts: Contact[] = [
    contact({
      id: 'c-shannon',
      displayName: 'Shannon Coach',
      phoneNumbers: ['+15551234567'],
    }),
  ];

  const result = rankConversationsByQuery(conversations, contacts, 'shannon');
  assert.equal(result.length, 1);
  assert.equal(result[0]!.conv.id, 'shannon');
  assert.equal(result[0]!.label, 'Shannon Coach');
});

test('exact contact-name match outranks substring contact match', () => {
  const conversations: Conversation[] = [
    conv({ id: 'sub', participants: ['+15550000001'] }),
    conv({ id: 'exact', participants: ['+15550000002'] }),
  ];
  const contacts: Contact[] = [
    contact({
      id: 'c1',
      displayName: 'Roshannon Lee',
      phoneNumbers: ['+15550000001'],
    }),
    contact({
      id: 'c2',
      displayName: 'Shannon',
      phoneNumbers: ['+15550000002'],
    }),
  ];

  const result = rankConversationsByQuery(conversations, contacts, 'shannon');
  assert.equal(result[0]!.conv.id, 'exact');
  assert.equal(result[1]!.conv.id, 'sub');
});

test('group chat displayName match still works (Tier B)', () => {
  const conversations: Conversation[] = [
    conv({
      id: 'group',
      displayName: 'Shannon Birthday Crew',
      isGroup: true,
      participants: ['+15550000001', '+15550000002'],
    }),
  ];
  const result = rankConversationsByQuery(conversations, [], 'shannon');
  assert.equal(result.length, 1);
  assert.equal(result[0]!.conv.id, 'group');
  assert.equal(result[0]!.label, 'Shannon Birthday Crew');
});

test('email handle matching uses literal lowercase comparison', () => {
  const conversations: Conversation[] = [
    conv({ id: 'shannon-email', participants: ['SHANNON@EXAMPLE.COM'] }),
  ];
  const contacts: Contact[] = [
    contact({
      id: 'c1',
      displayName: 'Shannon Coach',
      emails: ['shannon@example.com'],
    }),
  ];
  const result = rankConversationsByQuery(conversations, contacts, 'shannon');
  assert.equal(result.length, 1);
  assert.equal(result[0]!.conv.id, 'shannon-email');
});

test('contact match wins over server-side body matches (no contact entry → not returned)', () => {
  // The "noise" conversation contains "shannon" in its handle substring;
  // the contact-matched conversation should still come first.
  const conversations: Conversation[] = [
    conv({ id: 'noise', participants: ['shannon-bot@spam.io'] }),
    conv({ id: 'real', participants: ['+15551234567'] }),
  ];
  const contacts: Contact[] = [
    contact({
      id: 'c-shannon',
      displayName: 'Shannon',
      phoneNumbers: ['+15551234567'],
    }),
  ];
  const result = rankConversationsByQuery(conversations, contacts, 'shannon');
  assert.equal(result[0]!.conv.id, 'real');
  // 'noise' still appears via Tier C raw substring match, but ranks lower.
  assert.equal(result[1]!.conv.id, 'noise');
});

test('respects the max cap', () => {
  const conversations: Conversation[] = Array.from({ length: 10 }, (_, i) =>
    conv({ id: `c${i}`, displayName: `Shannon ${i}`, isGroup: true }),
  );
  const result = rankConversationsByQuery(conversations, [], 'shannon', 3);
  assert.equal(result.length, 3);
});
