import { describe, expect, test } from 'bun:test';
import { z } from 'zod';

import { createSendQueueRepo, sendQueueTableSql } from '../SendQueueRepo';

/** In-memory fake satisfying the subset of `Pool` we touch. */
class FakePool {
  rows: Array<Record<string, unknown>> = [];
  calls: Array<{ sql: string; params: unknown[] }> = [];

  async query<T = Record<string, unknown>>(sql: string, params: unknown[] = []) {
    this.calls.push({ sql, params });

    if (sql.startsWith('INSERT')) {
      const [deviceId, action, payloadJson] = params as [string, string, string];
      const row = {
        id: `id-${this.rows.length + 1}`,
        device_id: deviceId,
        action,
        payload: JSON.parse(payloadJson),
        status: 'queued',
        sent_at: null,
        failure_reason: null,
        created_at: new Date(Date.now() + this.rows.length).toISOString(),
        updated_at: new Date().toISOString(),
      };
      this.rows.push(row);
      return { rows: [{ id: row.id }] as unknown as T[] };
    }
    if (sql.includes('SELECT id, device_id')) {
      const [deviceId] = params as [string];
      const matching = this.rows
        .filter((r) => r.device_id === deviceId && r.status === 'queued')
        .sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)));
      return { rows: matching as unknown as T[] };
    }
    if (sql.includes("status = 'sent'")) {
      const [id] = params as [string];
      const row = this.rows.find((r) => r.id === id);
      if (row) {
        row.status = 'sent';
        row.sent_at = new Date().toISOString();
      }
      return { rows: [] as unknown as T[] };
    }
    if (sql.includes("status = 'failed'")) {
      const [id, reason] = params as [string, string | null];
      const row = this.rows.find((r) => r.id === id);
      if (row) {
        row.status = 'failed';
        row.failure_reason = reason;
      }
      return { rows: [] as unknown as T[] };
    }
    if (sql.includes('SELECT COUNT(*)')) {
      const [deviceId, status] = params as [string, string];
      const n = this.rows.filter((r) => r.device_id === deviceId && r.status === status).length;
      return { rows: [{ n: String(n) }] as unknown as T[] };
    }
    return { rows: [] as unknown as T[] };
  }
}

const payloadSchema = z.object({
  eventId: z.string(),
  title: z.string(),
});

describe('createSendQueueRepo', () => {
  test('enqueue → listPending round-trip preserves payload', async () => {
    const repo = createSendQueueRepo({
      tableName: 'macsync.calendar_send_queue',
      payloadSchema,
    });
    const pool = new FakePool();

    const { id } = await repo.enqueue(pool as unknown as never, {
      deviceId: 'dev-1',
      action: 'create_event',
      payload: { eventId: 'e1', title: 'Lunch' },
    });
    expect(id).toBe('id-1');

    const pending = await repo.listPending(pool as unknown as never, 'dev-1');
    expect(pending).toHaveLength(1);
    expect(pending[0].action).toBe('create_event');
    expect(pending[0].payload).toEqual({ eventId: 'e1', title: 'Lunch' });
  });

  test('payload validation rejects malformed input', async () => {
    const repo = createSendQueueRepo({
      tableName: 'macsync.calendar_send_queue',
      payloadSchema,
    });
    const pool = new FakePool();
    expect(
      repo.enqueue(pool as unknown as never, {
        deviceId: 'dev-1',
        action: 'create_event',
        payload: { eventId: 'e1' } as never,
      }),
    ).rejects.toThrow();
  });

  test('allowedActions rejects unknown actions', async () => {
    const repo = createSendQueueRepo({
      tableName: 'macsync.calendar_send_queue',
      payloadSchema,
      allowedActions: ['create_event', 'update_event'],
    });
    const pool = new FakePool();
    expect(
      repo.enqueue(pool as unknown as never, {
        deviceId: 'dev-1',
        action: 'delete_event',
        payload: { eventId: 'e1', title: 't' },
      }),
    ).rejects.toThrow(/unknown action/);
  });

  test('markResult sent / failed updates row and removes from pending list', async () => {
    const repo = createSendQueueRepo({
      tableName: 'macsync.calendar_send_queue',
      payloadSchema,
    });
    const pool = new FakePool();

    const { id: a } = await repo.enqueue(pool as unknown as never, {
      deviceId: 'dev-1',
      action: 'create_event',
      payload: { eventId: 'a', title: 't' },
    });
    const { id: b } = await repo.enqueue(pool as unknown as never, {
      deviceId: 'dev-1',
      action: 'create_event',
      payload: { eventId: 'b', title: 't' },
    });

    await repo.markResult(pool as unknown as never, { id: a, status: 'sent' });
    await repo.markResult(pool as unknown as never, {
      id: b,
      status: 'failed',
      error: 'EventKit denied',
    });

    const pending = await repo.listPending(pool as unknown as never, 'dev-1');
    expect(pending).toHaveLength(0);

    const sentCount = await repo.count(pool as unknown as never, 'dev-1', 'sent');
    const failedCount = await repo.count(pool as unknown as never, 'dev-1', 'failed');
    expect(sentCount).toBe(1);
    expect(failedCount).toBe(1);
  });

  test('invalid tableName is rejected at construction', () => {
    expect(() =>
      createSendQueueRepo({
        tableName: 'macsync.send_queue; DROP TABLE x',
        payloadSchema,
      }),
    ).toThrow();
  });
});

describe('sendQueueTableSql', () => {
  test('includes table + two indexes', () => {
    const sql = sendQueueTableSql('macsync.calendar_send_queue');
    expect(sql).toContain('CREATE TABLE IF NOT EXISTS macsync.calendar_send_queue');
    expect(sql).toContain('idx_macsync_calendar_send_queue_device_status');
    expect(sql).toContain('idx_macsync_calendar_send_queue_created');
  });

  test('rejects invalid identifiers', () => {
    expect(() => sendQueueTableSql('drop table x; --')).toThrow();
  });
});
