import { randomUUID } from 'node:crypto';
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { DlqService } from '../outbox/dlq.service';
import { resetDb } from '../test-utils/reset-db';
import { ProjectionCursorService } from '../projection/projection-cursor.service';
import { makeFakePorts, makeFakeInference } from '@insignia/iios-testkit';
import { IIOS_EVENTS, type CloudEvent } from '@insignia/iios-contracts';
import { MessageService, type MessagePrincipal } from '../messaging/message.service';
import { ActorResolver } from '../identity/actor.resolver';
import { AiJobService } from '../ai/ai.service';
import { AiBudgetGuard } from '../ai/ai-budget.guard';
import { OutboundService } from '../adapters/outbound.service';
import { IdempotencyService } from '../idempotency/idempotency.service';
import { CapabilityBroker } from '../capability/capability.broker';
import { CapabilityProviderRegistry } from '../capability/capability.registry';
import { CalendarService } from './calendar.service';
import { CalendarProjector } from './calendar.projector';
import { OutboxBus } from '../outbox/outbox.bus';
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 actors = new ActorResolver(asService);

const alice: MessagePrincipal = { userId: 'alice', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Alice' };
const START = '2026-07-02T10:00:00.000Z';

const ai = () => new AiJobService(asService, makeFakePorts(), makeFakeInference(), new AiBudgetGuard(asService), actors);
const cal = () => new CalendarService(asService, makeFakePorts(), actors, ai(), new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()), new IdempotencyService(asService)));

async function makeInteraction(text: string): Promise<string> {
  const m = new MessageService(asService, makeFakePorts(), actors);
  const { threadId } = await m.openThread(null, alice);
  const msg = await m.send(threadId, alice, { content: text }, `k-${randomUUID()}`);
  return msg.id;
}

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

describe('Calendar reminders + projector (P8)', () => {
  it('schedule → one reminder outbound + a SYSTEM_ALERT inbox item per participant, replay-safe', async () => {
    const m = await cal().scheduleDirect(alice, { meetingType: 'INTERNAL', title: 'Sync', startAt: START, attendees: [{ userId: 'bob' }] });
    expect(await prisma.iiosOutboundCommand.count({ where: { idempotencyKey: `meeting-reminder:${m.id}` } })).toBe(1);
    expect(await prisma.iiosInboxItem.count({ where: { kind: 'SYSTEM_ALERT' } })).toBe(2);

    await cal().queueReminders(m.id); // replay
    expect(await prisma.iiosOutboundCommand.count({ where: { idempotencyKey: `meeting-reminder:${m.id}` } })).toBe(1);
    expect(await prisma.iiosInboxItem.count({ where: { kind: 'SYSTEM_ALERT' } })).toBe(2);
  });

  it('accepting an AI EVENT artifact → exactly one meeting_request (idempotent replay)', async () => {
    const interactionId = await makeInteraction('Maybe a party at 9 PM?');
    const { artifact } = await ai().runJob({ interactionId, jobType: 'EXTRACT', principal: alice });
    await ai().accept(artifact!.id, alice);
    const event = (await prisma.iiosOutboxEvent.findFirstOrThrow({
      where: { eventType: IIOS_EVENTS.aiArtifactAccepted, aggregateId: artifact!.id },
    })).cloudEvent as unknown as CloudEvent;

    const p = new CalendarProjector(asService, new OutboxBus(), new DlqService(asService), new ProjectionCursorService(asService));
    await p.onArtifactAccepted(event);
    await p.onArtifactAccepted(event); // replay
    const claim = await prisma.iiosAiClaim.findFirstOrThrow({ where: { artifactId: artifact!.id, claimType: 'EVENT' } });
    expect(await prisma.iiosMeetingRequest.count({ where: { sourceClaimId: claim.id } })).toBe(1);
  });
});
