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.service';
import { AiBudgetGuard } from './ai-budget.guard';
import type { PrismaService } from '../prisma/prisma.service';
import type { FakeInference } from '@insignia/iios-testkit';
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 user: MessagePrincipal = { userId: 'admin-1', orgId: 'org_demo', appId: 'portal-demo', displayName: 'Admin' };

function svc(opts?: { ports?: FakePorts; inference?: FakeInference; quota?: number }) {
  const ports = opts?.ports ?? makeFakePorts();
  const inference = opts?.inference ?? makeFakeInference();
  const budget = new AiBudgetGuard(asService);
  if (opts?.quota !== undefined) budget.setQuota(opts.quota);
  return new AiJobService(asService, ports, inference, budget, actors);
}

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

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

describe('AiJobService (P7 proposal layer)', () => {
  it('CLASSIFY "party at 9 PM" → CLASSIFICATION artifact + MODERATION_FLAG claim + AI moderation flag', async () => {
    const interactionId = await makeInteraction('There is a party at 9 PM tonight!');
    const { job, artifact } = await svc().runJob({ interactionId, jobType: 'CLASSIFY', principal: user });

    expect(job.status).toBe('DONE');
    expect(artifact?.artifactType).toBe('CLASSIFICATION');
    expect(artifact?.claims.some((c) => c.claimType === 'MODERATION_FLAG')).toBe(true);
    const flag = await prisma.iiosModerationFlag.findFirst({ where: { interactionId, proposedBy: 'AI' } });
    expect(flag?.flagType).toBe('AFTER_HOURS_EVENT');
    expect(flag?.confidence).toBeGreaterThan(0);
    expect(flag?.explanation).toBeTruthy();
  });

  it('SUMMARIZE → SUMMARY artifact with a cited evidence link', async () => {
    const interactionId = await makeInteraction('The committee met today. Budget discussed.');
    const { artifact } = await svc().runJob({ interactionId, jobType: 'SUMMARIZE', principal: user });
    expect(artifact?.artifactType).toBe('SUMMARY');
    expect((artifact?.contentRef as { text?: string }).text).toContain('[ai]');
    expect(artifact?.evidence.some((e) => e.sourceId === interactionId)).toBe(true);
  });

  it('EXTRACT → EVENT claim stays certainty=DISCUSSION (Scenario 7)', async () => {
    const interactionId = await makeInteraction('Maybe a party at 9 PM?');
    const { artifact } = await svc().runJob({ interactionId, jobType: 'EXTRACT', principal: user });
    const event = artifact?.claims.find((c) => c.claimType === 'EVENT');
    expect((event?.claimJson as { certainty?: string }).certainty).toBe('DISCUSSION');
  });

  it('fails closed: OPA deny → FAILED job, PolicyDeniedError, 0 artifacts (KG-03)', async () => {
    const interactionId = await makeInteraction('hello');
    const ports = makeFakePorts();
    ports.opa.deny('no ai purpose');
    await expect(svc({ ports }).runJob({ interactionId, jobType: 'CLASSIFY', principal: user })).rejects.toBeInstanceOf(PolicyDeniedError);
    expect(await prisma.iiosAiArtifact.count()).toBe(0);
    expect((await prisma.iiosAiJob.findFirstOrThrow({ where: { interactionId } })).status).toBe('FAILED');
  });

  it('budget governor: quota 0 → ABSTAINED job, 0 artifacts (KG-12 degrade)', async () => {
    const interactionId = await makeInteraction('The committee met today.');
    const { job, artifact } = await svc({ quota: 0 }).runJob({ interactionId, jobType: 'SUMMARIZE', principal: user });
    expect(job.status).toBe('ABSTAINED');
    expect(job.abstentionReason).toBe('BUDGET_EXCEEDED');
    expect(artifact).toBeNull();
    expect(await prisma.iiosAiArtifact.count()).toBe(0);
  });

  it('is idempotent: replaying the same job key twice → exactly 1 artifact', async () => {
    const interactionId = await makeInteraction('There is a party at 9 PM tonight!');
    const s = svc();
    const key = `ai:CLASSIFY:${interactionId}`;
    await s.runJob({ interactionId, jobType: 'CLASSIFY', principal: user, idempotencyKey: key });
    await s.runJob({ interactionId, jobType: 'CLASSIFY', principal: user, idempotencyKey: key });
    expect(await prisma.iiosAiJob.count({ where: { interactionId } })).toBe(1);
    expect(await prisma.iiosAiArtifact.count()).toBe(1);
  });

  it('accept records human feedback (ACCEPTED + claims validated) and emits an event', async () => {
    const interactionId = await makeInteraction('There is a party at 9 PM tonight!');
    const { artifact } = await svc().runJob({ interactionId, jobType: 'CLASSIFY', principal: user });
    const updated = await svc().accept(artifact!.id, user);
    expect(updated.status).toBe('ACCEPTED');
    expect(updated.acceptedByActorId).toBeTruthy();
    const claim = await prisma.iiosAiClaim.findFirst({ where: { artifactId: artifact!.id } });
    expect(claim?.validationStatus).toBe('ACCEPTED');
    const evt = await prisma.iiosOutboxEvent.findFirst({ where: { eventType: 'com.insignia.iios.ai.artifact.accepted.v1', aggregateId: artifact!.id } });
    expect(evt).toBeTruthy();
  });
});
