import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { resetDb } from '../test-utils/reset-db';
import type { CloudEvent } from '@insignia/iios-contracts';
import { DlqService } from './dlq.service';
import type { PrismaService } from '../prisma/prisma.service';

const url = process.env.DATABASE_URL ?? 'postgresql://iios:iios@localhost:5434/iios?schema=public';
const prisma = new PrismaClient({ datasources: { db: { url } } });
const asService = prisma as unknown as PrismaService;

const evt = (id: string): CloudEvent => ({
  specversion: '1.0', id, type: 'x', source: 's', time: new Date().toISOString(),
  insignia: { scopeSnapshotId: 'scope-A', idempotencyKey: `k:${id}` }, data: {},
});

beforeAll(async () => { await prisma.$connect(); });
afterAll(async () => { await prisma.$disconnect(); });
beforeEach(async () => { await resetDb(prisma); });

// A DLQ item's replay needs the underlying outbox event to exist.
async function seedOutboxEvent(eventId: string): Promise<void> {
  await prisma.iiosOutboxEvent.create({
    data: { eventId, aggregateType: 'x', aggregateId: eventId, eventType: 'x', cloudEvent: evt(eventId) as unknown as object, partitionKey: 'p' },
  });
}

describe('DlqService (P9 — dead-letter + replay)', () => {
  it('record is idempotent per (consumer, sourceId) and bumps retryCount', async () => {
    const dlq = new DlqService(asService);
    await dlq.record({ sourceType: 'projection', sourceId: 'e1', failureStage: 'c', consumerName: 'c', scopeId: 'scope-A' });
    await dlq.record({ sourceType: 'projection', sourceId: 'e1', failureStage: 'c', consumerName: 'c', scopeId: 'scope-A' });
    const items = await dlq.listDlq('scope-A');
    expect(items).toHaveLength(1);
    expect(items[0].retryCount).toBe(1); // 0 on create, +1 on repeat
  });

  it('onConsumerFailure quarantines the event and clears its claim (replayable)', async () => {
    const dlq = new DlqService(asService);
    await prisma.iiosProcessedEvent.create({ data: { consumerName: 'c', eventId: 'e2' } }); // claimed then failed
    await dlq.onConsumerFailure('c', evt('e2'), new Error('boom'));
    expect(await prisma.iiosDlqItem.count({ where: { sourceId: 'e2', consumerName: 'c' } })).toBe(1);
    expect(await prisma.iiosProcessedEvent.count({ where: { consumerName: 'c', eventId: 'e2' } })).toBe(0); // claim cleared
  });

  it('replay re-runs the registered handler → RESOLVED (only when it succeeds)', async () => {
    const dlq = new DlqService(asService);
    await seedOutboxEvent('e3');
    let calls = 0;
    dlq.registerHandler('c', async () => { calls++; if (calls === 1) throw new Error('still broken'); });
    await dlq.onConsumerFailure('c', evt('e3'), new Error('boom'));
    const item = await prisma.iiosDlqItem.findFirstOrThrow({ where: { sourceId: 'e3' } });

    const r1 = await dlq.replay(item.id); // handler throws again
    expect(r1.status).toBe('QUARANTINED');
    expect((await prisma.iiosDlqItem.findUniqueOrThrow({ where: { id: item.id } })).retryCount).toBe(1);

    const r2 = await dlq.replay(item.id); // handler now succeeds
    expect(r2.status).toBe('RESOLVED');
    expect((await prisma.iiosDlqItem.findUniqueOrThrow({ where: { id: item.id } })).resolvedAt).toBeTruthy();
  });
});
