/**
 * Repo unit tests for `searchMessages`.
 *
 * Seeds messages via `bulkUpsertMessages` directly against the real `icloud`
 * schema (the repo SQL uses hardcoded `icloud.` qualifiers). Each test uses
 * UUID-unique word prefixes to guarantee cache misses and no collision with
 * production data. Seeded rows are removed in afterAll.
 *
 * Requires: QUINN_MACSYNC_DB_URL pointing at a reachable Postgres instance.
 */

import { describe, it, expect, beforeAll, afterAll } from 'bun:test';
import { randomUUIDv7 } from 'bun';

import { bulkUpsertMessages, searchMessages } from '@/entities/message/repo';
import { pgRun } from '@/shared/db';

import { setupTestDb, teardownTestDb, type TestDb } from './db-harness';

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

let testDb: TestDb;
const seededDeviceIds: string[] = [];

/** Returns a unique 8-char hex prefix safe for embedding in test words. */
function uq(): string {
  return randomUUIDv7().replace(/-/g, '').slice(0, 8);
}

async function seedDevice(): Promise<string> {
  const id = randomUUIDv7();
  await pgRun(
    testDb.pool,
    `INSERT INTO macsync.devices (id, name, token, platform)
     VALUES ($1, 'repo-test-device', $2, 'macos')`,
    [id, `tok-repo-${id}`],
  );
  seededDeviceIds.push(id);
  return id;
}

async function seedConversation(deviceId: string): Promise<string> {
  const id = randomUUIDv7();
  await pgRun(
    testDb.pool,
    `INSERT INTO macsync.conversations (id, device_id, external_id, display_name, is_group, participants)
     VALUES ($1, $2, $3, 'Repo Test Convo', false, '[]'::jsonb)`,
    [id, deviceId, `ext-repo-${id}`],
  );
  return id;
}

// ---------------------------------------------------------------------------
// Setup / teardown
// ---------------------------------------------------------------------------

beforeAll(async () => {
  testDb = await setupTestDb();
});

afterAll(async () => {
  if (seededDeviceIds.length > 0) {
    const placeholders = seededDeviceIds.map((_, i) => `$${i + 1}`).join(', ');
    await pgRun(
      testDb.pool,
      `DELETE FROM macsync.messages WHERE device_id IN (${placeholders})`,
      seededDeviceIds,
    );
    await pgRun(
      testDb.pool,
      `DELETE FROM macsync.conversations WHERE device_id IN (${placeholders})`,
      seededDeviceIds,
    );
    await pgRun(
      testDb.pool,
      `DELETE FROM macsync.devices WHERE id IN (${placeholders})`,
      seededDeviceIds,
    );
  }
  await teardownTestDb(testDb);
});

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

describe('searchMessages', () => {
  it('returns ranked hits — exact phrase ranks above single-word match', async () => {
    const deviceId = await seedDevice();
    const convId = await seedConversation(deviceId);
    const now = new Date().toISOString();

    // Two UUID-unique root words so this combination never appears in cache.
    const wordA = `bnmng${uq()}`;
    const wordB = `smthie${uq()}`;

    await bulkUpsertMessages(testDb.pool, [
      {
        id: randomUUIDv7(),
        deviceId,
        conversationId: convId,
        externalId: `guid-phrase-${randomUUIDv7()}`,
        provider: 'imessage',
        fromHandle: '+1555000001',
        body: `Let us meet at the ${wordA} ${wordB} smoothie bar`,
        isFromMe: false,
        hasAttachments: false,
        attachmentCount: 0,
        sentAt: now,
      },
      {
        id: randomUUIDv7(),
        deviceId,
        conversationId: convId,
        externalId: `guid-single-${randomUUIDv7()}`,
        provider: 'imessage',
        fromHandle: '+1555000002',
        body: `Just picked up some ${wordA} bread from the store`,
        isFromMe: true,
        hasAttachments: false,
        attachmentCount: 0,
        sentAt: now,
      },
    ]);

    // "wordA OR wordB" uses websearch_to_tsquery OR semantics — both messages match,
    // but the one containing both words should rank higher.
    const result = await searchMessages(testDb.pool, {
      query: `${wordA} OR ${wordB}`,
      limit: 10,
      mode: 'lexical',
    });

    expect(result.hits.length).toBeGreaterThanOrEqual(2);

    // Message containing BOTH words must rank ahead of message with only one.
    const phraseIdx = result.hits.findIndex((h) => h.message.body.includes(wordB));
    const singleIdx = result.hits.findIndex((h) => h.message.body.includes('bread'));

    expect(phraseIdx).toBeGreaterThanOrEqual(0);
    expect(singleIdx).toBeGreaterThanOrEqual(0);
    expect(phraseIdx).toBeLessThan(singleIdx);
  });

  it('returns an empty array when no messages match', async () => {
    const result = await searchMessages(testDb.pool, {
      query: `xyzzy${uq()} quux${uq()} frobnicate${uq()}`,
      limit: 10,
    });
    expect(result.hits).toEqual([]);
    expect(result.total).toBe(0);
  });

  it('respects the limit parameter', async () => {
    const deviceId = await seedDevice();
    const convId = await seedConversation(deviceId);
    const now = new Date().toISOString();
    const uniqueWord = `zephyr${uq()}`;

    const drafts = Array.from({ length: 5 }, (_, i) => ({
      id: randomUUIDv7(),
      deviceId,
      conversationId: convId,
      externalId: `guid-limit-${i}-${randomUUIDv7()}`,
      provider: 'imessage' as const,
      fromHandle: '+1555000099',
      body: `Unique search term ${uniqueWord} message number ${i}`,
      isFromMe: false,
      hasAttachments: false,
      attachmentCount: 0,
      sentAt: now,
    }));

    await bulkUpsertMessages(testDb.pool, drafts);

    const result = await searchMessages(testDb.pool, {
      query: uniqueWord,
      limit: 3,
      mode: 'lexical',
    });
    expect(result.hits.length).toBeLessThanOrEqual(3);
  });

  it('includes a non-empty snippet with «» highlight markers', async () => {
    const deviceId = await seedDevice();
    const convId = await seedConversation(deviceId);
    const now = new Date().toISOString();
    const uniqueWord = `turquoise${uq()}`;

    await bulkUpsertMessages(testDb.pool, [
      {
        id: randomUUIDv7(),
        deviceId,
        conversationId: convId,
        externalId: `guid-snip-${randomUUIDv7()}`,
        provider: 'imessage',
        fromHandle: '+1555000003',
        body: `The quick ${uniqueWord} fox jumped over the lazy wombat and kept running`,
        isFromMe: false,
        hasAttachments: false,
        attachmentCount: 0,
        sentAt: now,
      },
    ]);

    const result = await searchMessages(testDb.pool, {
      query: uniqueWord,
      limit: 5,
      mode: 'lexical',
    });
    expect(result.hits.length).toBeGreaterThanOrEqual(1);

    for (const hit of result.hits) {
      expect(hit.snippet.length).toBeGreaterThan(0);
      expect(hit.snippet).toMatch(/«[^»]+»/);
    }
  });

  it('returns a numeric rank for each hit', async () => {
    const deviceId = await seedDevice();
    const convId = await seedConversation(deviceId);
    const now = new Date().toISOString();
    const uniqueWord = `labyrinthine${uq()}`;

    await bulkUpsertMessages(testDb.pool, [
      {
        id: randomUUIDv7(),
        deviceId,
        conversationId: convId,
        externalId: `guid-rank-${randomUUIDv7()}`,
        provider: 'imessage',
        fromHandle: '+1555000004',
        body: `Galumphing through the ${uniqueWord} corridors of the old palace`,
        isFromMe: false,
        hasAttachments: false,
        attachmentCount: 0,
        sentAt: now,
      },
    ]);

    const result = await searchMessages(testDb.pool, {
      query: uniqueWord,
      limit: 5,
      mode: 'lexical',
    });
    expect(result.hits.length).toBeGreaterThanOrEqual(1);

    for (const hit of result.hits) {
      expect(typeof hit.rank).toBe('number');
      expect(hit.rank).toBeGreaterThan(0);
    }
  });

  it('total reflects the true match count regardless of limit', async () => {
    const deviceId = await seedDevice();
    const convId = await seedConversation(deviceId);
    const now = new Date().toISOString();
    const uniqueWord = `palimpsest${uq()}`;

    const drafts = Array.from({ length: 4 }, (_, i) => ({
      id: randomUUIDv7(),
      deviceId,
      conversationId: convId,
      externalId: `guid-total-${i}-${randomUUIDv7()}`,
      provider: 'imessage' as const,
      fromHandle: '+1555000055',
      body: `Message about ${uniqueWord} number ${i}`,
      isFromMe: false,
      hasAttachments: false,
      attachmentCount: 0,
      sentAt: now,
    }));
    await bulkUpsertMessages(testDb.pool, drafts);

    const resAll = await searchMessages(testDb.pool, {
      query: uniqueWord,
      limit: 100,
      mode: 'lexical',
    });
    const resOne = await searchMessages(testDb.pool, {
      query: uniqueWord,
      limit: 1,
      mode: 'lexical',
    });

    expect(resAll.total).toBe(resOne.total);
    expect(resAll.total).toBeGreaterThanOrEqual(4);
    expect(resOne.hits.length).toBeLessThanOrEqual(1);
  });
});
