/**
 * Unit tests for the outbox repo's SQL — drain ordering (priority desc,
 * not_before asc), due-filter, dedupe-on-conflict, and retry/terminal status
 * transitions. Capturing FakePool, no Postgres.
 *
 * NOTE: like calls.repo.test.ts this imports `@/shared/db`, which pulls the
 * private `@lilith/quinn-db-pg` package — run where that registry is reachable
 * (`bun install && bun test src/test/outbox.repo.test.ts`).
 */

import { describe, expect, test } from 'bun:test';
import type { Pool } from 'pg';

import { enqueueOutbox, listDue, markResult, requeueStaleSending } from '@/entities/outbox/repo';
import type { OutboxDraft } from '@/entities/outbox';

interface Captured {
  sql: string;
  params: unknown[];
}

class FakePool {
  calls: Captured[] = [];
  private readonly rows: Record<string, unknown>[];
  constructor(rows: Record<string, unknown>[] = []) {
    this.rows = rows;
  }
  async query<T = Record<string, unknown>>(sql: string, params: unknown[] = []) {
    this.calls.push({ sql, params });
    return { rows: this.rows as unknown as T[] };
  }
}

const draft = (over: Partial<OutboxDraft> = {}): OutboxDraft => ({
  deviceId: 'dev-1',
  recipient: '+14155550000',
  body: 'hi',
  priority: 50,
  channelPref: 'auto',
  notBefore: '2026-06-28T12:00:00.000Z',
  dedupeKey: 'dk-1',
  ...over,
});

describe('enqueueOutbox', () => {
  test('inserts with ON CONFLICT DO NOTHING and returns id (not deduped)', async () => {
    const pool = new FakePool([{ id: 'new-1' }]);
    const res = await enqueueOutbox(pool as unknown as Pool, draft());
    expect(res).toEqual({ id: 'new-1', deduped: false });
    expect(pool.calls[0].sql).toContain('ON CONFLICT (device_id, dedupe_key)');
    expect(pool.calls[0].sql).toContain('DO NOTHING');
  });

  test('on conflict (no insert row) falls back to existing id, deduped=true', async () => {
    // First query (insert) returns no rows; second (select existing) returns id.
    let n = 0;
    const pool = {
      calls: [] as Captured[],
      async query(sql: string, params: unknown[] = []) {
        this.calls.push({ sql, params });
        n += 1;
        return { rows: n === 1 ? [] : [{ id: 'existing-9' }] };
      },
    };
    const res = await enqueueOutbox(pool as unknown as Pool, draft());
    expect(res).toEqual({ id: 'existing-9', deduped: true });
    expect(pool.calls[1].sql).toContain('SELECT id FROM macsync.outbox');
  });
});

describe('listDue', () => {
  test('filters queued + not_before<=now and orders priority desc, not_before asc', async () => {
    const pool = new FakePool([]);
    await listDue(pool as unknown as Pool, 'dev-1', '2026-06-28T12:00:00.000Z', 10);
    const { sql, params } = pool.calls[0];
    expect(sql).toContain("status = 'queued'");
    expect(sql).toContain('not_before <= $2');
    expect(sql).toContain('ORDER BY priority DESC, not_before ASC');
    expect(params).toEqual(['dev-1', '2026-06-28T12:00:00.000Z', 10]);
  });
});

describe('markResult', () => {
  test('failed re-queues while attempts remain, else hard-fails — device-scoped', async () => {
    const pool = new FakePool([{ id: 'x' }]);
    const ok = await markResult(pool as unknown as Pool, { id: 'x', deviceId: 'dev-1', status: 'failed', error: 'error 22' });
    expect(ok).toBe(true);
    const { sql, params } = pool.calls[0];
    expect(sql).toContain("WHEN attempts >= max_attempts THEN 'failed' ELSE 'queued'");
    expect(sql).toContain('WHERE id = $1 AND device_id = $2');
    expect(params).toEqual(['x', 'dev-1', 'error 22', null]);
  });

  test('sent is terminal — sets sent_at and clears error', async () => {
    const pool = new FakePool([{ id: 'y' }]);
    const ok = await markResult(pool as unknown as Pool, { id: 'y', deviceId: 'dev-1', status: 'sent', channelUsed: 'sms' });
    expect(ok).toBe(true);
    const { sql, params } = pool.calls[0];
    expect(sql).toContain('sent_at = now()');
    expect(sql).toContain('error = NULL');
    expect(sql).toContain('WHERE id = $1 AND device_id = $2');
    expect(params).toEqual(['y', 'dev-1', 'sent', 'sms']);
  });

  test("IDOR guard: no owned row → returns false (surface maps to 404)", async () => {
    const pool = new FakePool([]); // UPDATE ... RETURNING matched nothing
    const ok = await markResult(pool as unknown as Pool, { id: 'other', deviceId: 'dev-1', status: 'sent' });
    expect(ok).toBe(false);
  });
});

describe('requeueStaleSending', () => {
  test('reclaims sending rows older than cutoff; requeues unless attempts exhausted', async () => {
    const pool = new FakePool([
      { id: 'a', status: 'queued' },
      { id: 'b', status: 'failed' },
    ]);
    const cutoff = '2026-06-28T11:50:00.000Z';
    const reclaimed = await requeueStaleSending(pool as unknown as Pool, cutoff);
    expect(reclaimed).toEqual([
      { id: 'a', status: 'queued' },
      { id: 'b', status: 'failed' },
    ]);
    const { sql, params } = pool.calls[0];
    expect(sql).toContain("status = 'sending' AND updated_at < $1");
    expect(sql).toContain("WHEN attempts >= max_attempts THEN 'failed' ELSE 'queued'");
    expect(params).toEqual([cutoff]);
  });

  test('nothing stale → empty array', async () => {
    const pool = new FakePool([]);
    const reclaimed = await requeueStaleSending(pool as unknown as Pool, '2026-06-28T11:50:00.000Z');
    expect(reclaimed).toEqual([]);
  });
});
