/**
 * Tests for `searchContacts` — the unified contact-name search helper that
 * also backs the `GET /my/contacts/by-name` endpoint via `queryContactsByName`.
 *
 * Seeds contacts directly via `upsertContact` against the `macsync.contacts`
 * table. Device rows are inserted first to satisfy the FK. Cleanup is via
 * cascade on device FK.
 *
 * 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 type { Hono } from 'hono';
import type { Pool } from 'pg';

import { upsertContact, searchContacts, updateContactSummary } from '@/entities/contact/repo';
import { pgRun } from '@/shared/db';
import { loadConfig } from '@/app/config';

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

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

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

function uq(): string {
  return randomUUIDv7().replace(/-/g, '').slice(0, 8);
}

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

async function seedContact(
  pool: Pool,
  deviceId: string,
  displayName: string,
  overrides: {
    phoneNumbers?: string[];
    emails?: string[];
    handle?: string;
  } = {},
): Promise<string> {
  const handle = overrides.handle ?? `+1555${uq()}`;
  const contact = await upsertContact(pool, {
    id: randomUUIDv7(),
    deviceId,
    handle,
    displayName,
    phoneNumbers: overrides.phoneNumbers ?? [],
    emails: overrides.emails ?? [],
  });
  return contact.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.contacts WHERE device_id IN (${placeholders})`,
      seededDeviceIds,
    );
    await pgRun(
      testDb.pool,
      `DELETE FROM macsync.devices WHERE id IN (${placeholders})`,
      seededDeviceIds,
    );
  }
  await teardownTestDb(testDb);
});

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

describe('searchContacts', () => {
  it('returns contacts matching display_name (substring, case-insensitive)', async () => {
    const tag = uq();
    const deviceId = await seedDevice(testDb.pool, `match-${tag}`);
    await seedContact(testDb.pool, deviceId, `Matching Person ${tag}`);
    await seedContact(testDb.pool, deviceId, `Unrelated Contact ${uq()}`);

    const results = await searchContacts(testDb.pool, {
      query: `matching person ${tag}`.toLowerCase(),
      limit: 10,
    });

    expect(results.length).toBe(1);
    expect(results[0]!.contact.displayName).toBe(`Matching Person ${tag}`);
    expect(results[0]!.matchField).toBe('display_name');
  });

  it('returns empty array when no contacts match', async () => {
    const deviceId = await seedDevice(testDb.pool, `nomatch-${uq()}`);
    await seedContact(testDb.pool, deviceId, `SomeOtherName ${uq()}`);

    const results = await searchContacts(testDb.pool, {
      query: `zzz-no-such-contact-${uq()}`,
      limit: 10,
    });

    expect(results).toEqual([]);
  });

  it('respects the limit parameter', async () => {
    const tag = uq();
    const deviceId = await seedDevice(testDb.pool, `limit-${tag}`);
    for (let i = 0; i < 5; i++) {
      await seedContact(testDb.pool, deviceId, `LimitTest ${tag} ${i}`);
    }

    const results = await searchContacts(testDb.pool, {
      query: `LimitTest ${tag}`,
      limit: 3,
    });

    expect(results.length).toBe(3);
  });

  it('filters by deviceId when provided', async () => {
    const tag = uq();
    const deviceA = await seedDevice(testDb.pool, `dev-a-${tag}`);
    const deviceB = await seedDevice(testDb.pool, `dev-b-${tag}`);

    await seedContact(testDb.pool, deviceA, `DeviceFilter ${tag} A`);
    await seedContact(testDb.pool, deviceB, `DeviceFilter ${tag} B`);

    const results = await searchContacts(testDb.pool, {
      query: `DeviceFilter ${tag}`,
      deviceId: deviceA,
      limit: 10,
    });

    expect(results.length).toBe(1);
    expect(results[0]!.contact.deviceId).toBe(deviceA);
    expect(results[0]!.contact.displayName).toBe(`DeviceFilter ${tag} A`);
  });

  it('ranks exact match > prefix > substring', async () => {
    const tag = uq();
    const deviceId = await seedDevice(testDb.pool, `rank-${tag}`);

    // Three contacts that all match "alex":
    //   exact     → "Alex"  (display_name = query)
    //   prefix    → "Alexandra Smith"  (display_name LIKE 'alex%')
    //   substring → "Mr Alex Jr"  (display_name LIKE '%alex%' but not prefix)
    await seedContact(testDb.pool, deviceId, `Alex`);
    await seedContact(testDb.pool, deviceId, `Alexandra Smith ${tag}`);
    await seedContact(testDb.pool, deviceId, `Mr Alex Jr ${tag}`);

    const results = await searchContacts(testDb.pool, {
      query: `Alex`,
      deviceId,
      limit: 10,
    });

    expect(results.length).toBe(3);
    expect(results[0]!.contact.displayName).toBe(`Alex`);
    expect(results[0]!.matchStrength).toBe('exact');
    expect(results[1]!.contact.displayName).toBe(`Alexandra Smith ${tag}`);
    expect(results[1]!.matchStrength).toBe('prefix');
    expect(results[2]!.contact.displayName).toBe(`Mr Alex Jr ${tag}`);
    expect(results[2]!.matchStrength).toBe('substring');
    // Strict rank ordering
    expect(results[0]!.rank).toBeGreaterThan(results[1]!.rank);
    expect(results[1]!.rank).toBeGreaterThan(results[2]!.rank);
  });

  it('matches phone numbers with digit normalization', async () => {
    const tag = uq();
    const deviceId = await seedDevice(testDb.pool, `phone-${tag}`);

    // Stored normalized; query in formatted form should still match
    await seedContact(testDb.pool, deviceId, `PhoneOnly ${tag}`, {
      phoneNumbers: [`+14155551${tag.slice(0, 3)}`],
    });

    const results = await searchContacts(testDb.pool, {
      query: `(415) 555-1${tag.slice(0, 3)}`,
      deviceId,
      limit: 10,
    });

    expect(results.length).toBe(1);
    expect(results[0]!.matchField).toBe('phone');
  });

  it('matches emails as a substring of the emails jsonb', async () => {
    const tag = uq();
    const deviceId = await seedDevice(testDb.pool, `email-${tag}`);

    await seedContact(testDb.pool, deviceId, `EmailOnly ${tag}`, {
      emails: [`person-${tag}@example.com`],
    });

    const results = await searchContacts(testDb.pool, {
      query: `person-${tag}`,
      deviceId,
      limit: 10,
    });

    expect(results.length).toBe(1);
    expect(results[0]!.matchField).toBe('email');
  });
});

// ---------------------------------------------------------------------------
// Route-level: GET /my/contacts/by-name — summaryData in response
// ---------------------------------------------------------------------------

describe('GET /my/contacts/by-name — summaryData field', () => {
  let app: Hono;
  let serviceToken: string;
  const routeDeviceIds: string[] = [];

  beforeAll(async () => {
    const { createApp } = await import('@/app/server');
    const built = await createApp();
    app = built.app;
    serviceToken = loadConfig().SERVICE_TOKEN;
  });

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

  async function seedRouteDevice(label: string): Promise<string> {
    const id = randomUUIDv7();
    await pgRun(
      testDb.pool,
      `INSERT INTO macsync.devices (id, name, token, platform)
       VALUES ($1, $2, $3, 'macos')`,
      [id, `byname-route-${label}`, `tok-route-byname-${id}`],
    );
    routeDeviceIds.push(id);
    return id;
  }

  async function get(path: string): Promise<Response> {
    return app.fetch(
      new Request(`http://localhost${path}`, {
        headers: { Authorization: `Bearer ${serviceToken}` },
      }),
    );
  }

  it('response includes summaryData when contact has summary data', async () => {
    const tag = uq();
    const deviceId = await seedRouteDevice(`with-summary-${tag}`);
    const contact = await upsertContact(testDb.pool, {
      id: randomUUIDv7(),
      deviceId,
      handle: `+1888${tag.slice(0, 7)}`,
      displayName: `SummaryPresent ${tag}`,
      phoneNumbers: [],
      emails: [],
    });

    await updateContactSummary(testDb.pool, contact.id, {
      recap: 'Regular client, always on time',
      overallSummary: 'Long-term client who books monthly',
      mostRecently: 'Discussed scheduling for next month',
    });

    const res = await get(`/my/contacts/by-name?name=${encodeURIComponent(`SummaryPresent ${tag}`)}`);
    expect(res.status).toBe(200);

    const body = await res.json() as { contacts: Array<{ summaryData: unknown }> };
    expect(body.contacts).toHaveLength(1);
    expect(body.contacts[0]!.summaryData).toEqual({
      recap: 'Regular client, always on time',
      overallSummary: 'Long-term client who books monthly',
      mostRecently: 'Discussed scheduling for next month',
    });
  });

  it('response has summaryData: null when contact has no summary data', async () => {
    const tag = uq();
    const deviceId = await seedRouteDevice(`no-summary-${tag}`);
    await upsertContact(testDb.pool, {
      id: randomUUIDv7(),
      deviceId,
      handle: `+1777${tag.slice(0, 7)}`,
      displayName: `SummaryAbsent ${tag}`,
      phoneNumbers: [],
      emails: [],
    });

    const res = await get(`/my/contacts/by-name?name=${encodeURIComponent(`SummaryAbsent ${tag}`)}`);
    expect(res.status).toBe(200);

    const body = await res.json() as { contacts: Array<{ summaryData: unknown }> };
    expect(body.contacts).toHaveLength(1);
    expect(body.contacts[0]!.summaryData).toBeNull();
  });
});
