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 { ActorResolver } from '../identity/actor.resolver';
import { IngestService } from '../interactions/ingest.service';
import { RuleEngine } from '../routing/rule-engine';
import { RouteSimulator } from '../routing/route-simulator';
import { AiJobService } from './ai.service';
import { AiBudgetGuard } from './ai-budget.guard';
import { AiProjector } from './ai.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 ai = () => new AiJobService(asService, makeFakePorts(), makeFakeInference(), new AiBudgetGuard(asService), actors);
const sim = () => new RouteSimulator(asService, new RuleEngine());

async function ingestInbound(text: string): Promise<{ interactionId: string; scopeId: string; event: CloudEvent }> {
  const ingest = new IngestService(asService, makeFakePorts(), actors);
  const res = await ingest.ingest(
    {
      scope: { orgId: 'org_demo', appId: 'portal-demo' },
      channel: { type: 'WEBHOOK', externalChannelId: 'webhook' },
      source: { handleKind: 'EXTERNAL_VISITOR', externalId: 'ext-1' },
      kind: 'MESSAGE',
      thread: { externalThreadId: `wh-${randomUUID().slice(0, 6)}` },
      parts: [{ kind: 'TEXT', bodyText: text }],
      occurredAt: new Date().toISOString(),
      providerEventId: `pe-${randomUUID()}`,
    },
    `ing-${randomUUID()}`,
  );
  const interaction = await prisma.iiosInteraction.findUniqueOrThrow({ where: { id: res.interactionId } });
  const ev = (await prisma.iiosOutboxEvent.findFirstOrThrow({
    where: { eventType: IIOS_EVENTS.interactionNormalized, aggregateId: res.interactionId },
  })).cloudEvent as unknown as CloudEvent;
  return { interactionId: res.interactionId, scopeId: interaction.scopeId, event: ev };
}

function binding(scopeId: string, destinationRef: string, opts: { restriction?: string; requiresReview?: boolean; outputFormat?: 'FORWARD' | 'SUMMARY' } = {}) {
  return prisma.iiosRouteBinding.create({
    data: {
      scopeId,
      originChannelType: 'WEBHOOK',
      originRef: 'webhook',
      destinationChannelType: 'PORTAL',
      destinationRef,
      restrictionProfile: opts.restriction,
      requiresReview: opts.requiresReview ?? false,
      outputFormat: opts.outputFormat ?? 'FORWARD',
      mode: 'MANUAL',
      enabled: true,
    },
  });
}

const stateFor = async (interactionId: string, destinationRef: string) => {
  const b = await prisma.iiosRouteBinding.findFirstOrThrow({ where: { destinationRef } });
  return (await prisma.iiosRouteDecision.findUniqueOrThrow({ where: { interactionId_routeBindingId: { interactionId, routeBindingId: b.id } } })).decisionState;
};

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

describe('AI × routing (P7, KG-05: AI proposes, never approves)', () => {
  it('an AI flag forces REVIEW on an otherwise-auto-ALLOW binding — never ALLOW', async () => {
    const { interactionId, scopeId } = await ingestInbound('There is a party at 9 PM tonight!');
    // requiresReview:false + no rule match would be ALLOW — but CLASSIFY proposes an AI flag.
    await binding(scopeId, 'adults', { requiresReview: false });
    await ai().runJob({ interactionId, jobType: 'CLASSIFY' });

    await sim().simulate(interactionId, { channelType: 'WEBHOOK', ref: 'webhook' });
    expect(await stateFor(interactionId, 'adults')).toBe('REVIEW');
  });

  it('AI flag on a restricted (CHILD) destination → DENY (deny-by-default)', async () => {
    const { interactionId, scopeId } = await ingestInbound('There is a party at 9 PM tonight!');
    await binding(scopeId, 'children', { restriction: 'CHILD' });
    await ai().runJob({ interactionId, jobType: 'CLASSIFY' });

    await sim().simulate(interactionId, { channelType: 'WEBHOOK', ref: 'webhook' });
    expect(await stateFor(interactionId, 'children')).toBe('DENY');
  });

  it('SUMMARY binding renders the AI summary into the preview — and still sends nothing', async () => {
    const { interactionId, scopeId } = await ingestInbound('The committee met today. Budget discussed.');
    await binding(scopeId, 'digest-lane', { outputFormat: 'SUMMARY' });
    await ai().runJob({ interactionId, jobType: 'SUMMARIZE' });

    const { decisions } = await sim().simulate(interactionId, { channelType: 'WEBHOOK', ref: 'webhook' });
    const preview = decisions[0]!.previewPayload as { text: string; aiSourced: boolean };
    expect(preview.text).toContain('[ai]');
    expect(preview.aiSourced).toBe(true);
    expect(await prisma.iiosOutboundCommand.count()).toBe(0); // preview only, no send
  });

  it('AiProjector auto-classifies a channel-bearing interaction, idempotently', async () => {
    const { interactionId, event } = await ingestInbound('There is a party at 9 PM tonight!');
    const p = new AiProjector(asService, new OutboxBus(), new DlqService(asService), ai(), new ProjectionCursorService(asService));
    await p.onNormalized(event);
    await p.onNormalized(event); // replay
    expect(await prisma.iiosAiJob.count({ where: { interactionId, jobType: 'CLASSIFY' } })).toBe(1);
    expect(await prisma.iiosModerationFlag.count({ where: { interactionId, proposedBy: 'AI' } })).toBe(1);
  });
});
