import { randomUUID } from 'node:crypto';
import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest';
import { PrismaClient } from '@prisma/client';
import { resetDb } from '../test-utils/reset-db';
import { makeFakePorts, makeFakeInference } from '@insignia/iios-testkit';
import { PolicyDeniedError } 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 type { PrismaService } from '../prisma/prisma.service';
import type { FakePorts } from '@insignia/iios-testkit';

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 outbound = () => new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()), new IdempotencyService(asService));
const cal = (ports?: FakePorts) => new CalendarService(asService, ports ?? makeFakePorts(), actors, ai(), outbound());

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('CalendarService (P8 — 3 genesis paths → schedule)', () => {
  it('direct schedule → SCHEDULED meeting + organizer/attendee participants + calendar_event', async () => {
    const m = await cal().scheduleDirect(alice, {
      meetingType: 'INTERNAL',
      title: 'Weekly sync',
      startAt: START,
      attendees: [{ userId: 'bob', displayName: 'Bob' }],
    });
    expect(m.status).toBe('SCHEDULED');
    expect(m.participants).toHaveLength(2);
    expect(m.participants.some((p) => p.role === 'ORGANIZER')).toBe(true);
    expect(m.calendarEventId).toBeTruthy();
  });

  it('callback→meeting bridge populates IiosCallbackRequest.meetingRef', async () => {
    const scope = await actors.resolveScope(alice);
    const actor = await actors.resolveActor(scope.id, alice);
    const cb = await prisma.iiosCallbackRequest.create({
      data: { scopeId: scope.id, requesterActorId: actor.id, preferChannel: 'PHONE', status: 'PENDING' },
    });
    const req = await cal().fromCallback(alice, cb.id, { when: 'tomorrow' });
    expect(req.callbackRequestId).toBe(cb.id);
    const m = await cal().schedule(alice, { requestId: req.id, meetingType: 'CALLBACK', title: 'Callback with customer', startAt: START });
    const reloaded = await prisma.iiosCallbackRequest.findUniqueOrThrow({ where: { id: cb.id } });
    expect(reloaded.meetingRef).toBe(m.id);
  });

  it('AI path: an accepted EVENT claim becomes a meeting_request', 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 claim = await prisma.iiosAiClaim.findFirstOrThrow({ where: { artifactId: artifact!.id, claimType: 'EVENT' } });

    const req = await cal().fromEventClaim(alice, claim.id);
    expect(req.sourceClaimId).toBe(claim.id);
    const m = await cal().schedule(alice, { requestId: req.id, meetingType: 'INTERNAL', title: 'Party planning', startAt: START });
    expect(m.status).toBe('SCHEDULED');
  });

  it('AI path refuses an un-accepted EVENT claim (human confirmation required)', async () => {
    const interactionId = await makeInteraction('Maybe a party at 9 PM?');
    const { artifact } = await ai().runJob({ interactionId, jobType: 'EXTRACT', principal: alice });
    const claim = await prisma.iiosAiClaim.findFirstOrThrow({ where: { artifactId: artifact!.id, claimType: 'EVENT' } });
    await expect(cal().fromEventClaim(alice, claim.id)).rejects.toThrow();
  });

  it('fails closed: opa deny → PolicyDeniedError, no meeting', async () => {
    const ports = makeFakePorts();
    ports.opa.deny('no meeting scheduling');
    await expect(cal(ports).scheduleDirect(alice, { meetingType: 'INTERNAL', title: 'x', startAt: START })).rejects.toBeInstanceOf(PolicyDeniedError);
    expect(await prisma.iiosMeeting.count()).toBe(0);
  });
});
