import { JobsService } from '../jobs/jobs.service';
import type { XiaobaoRiskSummaryPayload } from '../xiaobao/xiaobao-risk.types';
import { XiaobaoAiService } from './xiaobao-ai.service';
import { XIAOBAO_AI_INTERPRET_JOB } from './xiaobao-ai.types';

describe('XiaobaoAiService', () => {
  function makeService() {
    const prisma = {
      xiaobaoRiskSummary: {
        findUnique: jest.fn(),
      },
      xiaobaoRiskInsight: {
        findFirst: jest.fn(),
        create: jest.fn(),
      },
      version: { update: jest.fn() },
      devTask: { update: jest.fn() },
      testCase: { update: jest.fn() },
      bug: { update: jest.fn() },
      requirement: { update: jest.fn() },
    };
    const jobs = {
      enqueue: jest.fn().mockResolvedValue({ id: 'job-1', status: 'queued' }),
    };
    const ai = {
      interpretRisk: jest.fn(),
    };
    return {
      prisma,
      jobs,
      ai,
      service: new XiaobaoAiService(prisma as any, jobs as unknown as JobsService, ai as any),
    };
  }

  it('enqueues an AI interpretation job for high-risk summaries without reusable cache', async () => {
    const { prisma, jobs, service } = makeService();
    const summary = buildSummary({ riskLevel: 'at_risk', riskScore: 68, riskSignature: 'version-1|at_risk|68' });
    prisma.xiaobaoRiskInsight.findFirst.mockResolvedValue(null);

    const result = await service.evaluateSummary(summary, { now: new Date('2026-07-08T08:00:00.000Z') });

    expect(result).toEqual({ enqueued: true, reason: 'queued', jobId: 'job-1' });
    expect(jobs.enqueue).toHaveBeenCalledWith({
      type: XIAOBAO_AI_INTERPRET_JOB,
      dedupeKey: 'version-1:version-1|at_risk|68',
      payload: { versionId: 'version-1', riskSignature: 'version-1|at_risk|68' },
      maxAttempts: 3,
    });
  });

  it('skips enqueue when the same risk signature already has a generated insight', async () => {
    const { prisma, jobs, service } = makeService();
    const summary = buildSummary({ riskSignature: 'version-1|blocked|90' });
    prisma.xiaobaoRiskInsight.findFirst.mockResolvedValueOnce({
      versionId: 'version-1',
      riskSignature: 'version-1|blocked|90',
      createdAt: new Date('2026-07-08T07:00:00.000Z'),
    });

    const result = await service.evaluateSummary(summary, { now: new Date('2026-07-08T08:00:00.000Z') });

    expect(result).toEqual({ enqueued: false, reason: 'cache_hit' });
    expect(jobs.enqueue).not.toHaveBeenCalled();
  });

  it('respects the six-hour cooldown when risk level has not escalated', async () => {
    const { prisma, jobs, service } = makeService();
    const summary = buildSummary({ riskLevel: 'at_risk', riskScore: 70, riskSignature: 'version-1|at_risk|70' });
    prisma.xiaobaoRiskInsight.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce({
        versionId: 'version-1',
        riskSignature: 'version-1|at_risk|64',
        createdAt: new Date('2026-07-08T06:00:00.000Z'),
      });

    const result = await service.evaluateSummary(summary, { now: new Date('2026-07-08T08:00:00.000Z') });

    expect(result).toEqual({ enqueued: false, reason: 'cooldown' });
    expect(jobs.enqueue).not.toHaveBeenCalled();
  });

  it('bypasses cooldown when the risk level escalates', async () => {
    const { prisma, jobs, service } = makeService();
    const summary = buildSummary({ riskLevel: 'blocked', riskScore: 92, riskSignature: 'version-1|blocked|92' });
    prisma.xiaobaoRiskInsight.findFirst
      .mockResolvedValueOnce(null)
      .mockResolvedValueOnce({
        versionId: 'version-1',
        riskSignature: 'version-1|at_risk|70',
        createdAt: new Date('2026-07-08T06:00:00.000Z'),
      });

    const result = await service.evaluateSummary(summary, { now: new Date('2026-07-08T08:00:00.000Z') });

    expect(result).toEqual({ enqueued: true, reason: 'risk_escalated', jobId: 'job-1' });
    expect(jobs.enqueue).toHaveBeenCalled();
  });

  it('does not enqueue on-track summaries', async () => {
    const { jobs, service } = makeService();

    const result = await service.evaluateSummary(buildSummary({ riskLevel: 'on_track', riskScore: 10 }), {
      now: new Date('2026-07-08T08:00:00.000Z'),
    });

    expect(result).toEqual({ enqueued: false, reason: 'policy_skip' });
    expect(jobs.enqueue).not.toHaveBeenCalled();
  });

  it('generates and stores an insight cache item without mutating business entities', async () => {
    const { prisma, ai, service } = makeService();
    const now = new Date('2026-07-08T08:00:00.000Z');
    const summary = buildSummary({ riskLevel: 'blocked', riskScore: 90, riskSignature: 'version-1|blocked|90' });
    prisma.xiaobaoRiskSummary.findUnique.mockResolvedValue({
      versionId: 'version-1',
      riskSignature: summary.riskSignature,
      summary,
    });
    prisma.xiaobaoRiskInsight.findFirst.mockResolvedValue(null);
    ai.interpretRisk.mockResolvedValue({
      ok: true,
      result: {
        summary: '存在发布阻塞',
        why: ['关键缺陷未关闭'],
        forecast: '预计延期',
        suggestedActions: ['优先修复阻塞项'],
        ownerHints: ['测试负责人跟进复测'],
        generatedAt: 'model-time-should-not-be-cache-time',
      },
      meta: { model: 'test-model', inputTokens: 10, outputTokens: 20, durationMs: 100 },
    });

    await service.runInterpretation(
      { versionId: 'version-1', riskSignature: 'version-1|blocked|90' },
      { now },
    );

    expect(ai.interpretRisk).toHaveBeenCalledWith(expect.objectContaining({
      versionId: 'version-1',
      riskLevel: 'blocked',
      reasons: [{ key: 'critical_bug', label: '关键缺陷', severity: 'critical', detail: '仍有关键缺陷' }],
    }));
    expect(prisma.xiaobaoRiskInsight.create).toHaveBeenCalledWith({
      data: {
        versionId: 'version-1',
        riskSignature: 'version-1|blocked|90',
        status: 'generated',
        insight: {
          versionId: 'version-1',
          riskSignature: 'version-1|blocked|90',
          generatedAt: now.toISOString(),
          insight: expect.objectContaining({ summary: '存在发布阻塞' }),
          providerInfo: { model: 'test-model' },
        },
        createdAt: now,
      },
    });
    expect(prisma.version.update).not.toHaveBeenCalled();
    expect(prisma.devTask.update).not.toHaveBeenCalled();
    expect(prisma.testCase.update).not.toHaveBeenCalled();
    expect(prisma.bug.update).not.toHaveBeenCalled();
    expect(prisma.requirement.update).not.toHaveBeenCalled();
  });

  it('skips stale interpretation jobs when the summary signature has moved on', async () => {
    const { prisma, ai, service } = makeService();
    prisma.xiaobaoRiskSummary.findUnique.mockResolvedValue({
      versionId: 'version-1',
      riskSignature: 'version-1|blocked|95',
      summary: buildSummary({ riskSignature: 'version-1|blocked|95' }),
    });

    await service.runInterpretation({ versionId: 'version-1', riskSignature: 'version-1|blocked|90' });

    expect(ai.interpretRisk).not.toHaveBeenCalled();
    expect(prisma.xiaobaoRiskInsight.create).not.toHaveBeenCalled();
  });

  it('throws when AI interpretation fails so the background job can retry', async () => {
    const { prisma, ai, service } = makeService();
    const summary = buildSummary({ riskSignature: 'version-1|blocked|90' });
    prisma.xiaobaoRiskSummary.findUnique.mockResolvedValue({
      versionId: 'version-1',
      riskSignature: summary.riskSignature,
      summary,
    });
    prisma.xiaobaoRiskInsight.findFirst.mockResolvedValue(null);
    ai.interpretRisk.mockResolvedValue({ ok: false, code: 'API_ERROR', error: 'provider down' });

    await expect(service.runInterpretation({ versionId: 'version-1', riskSignature: summary.riskSignature }))
      .rejects.toThrow('provider down');
    expect(prisma.xiaobaoRiskInsight.create).not.toHaveBeenCalled();
  });
});

function buildSummary(patch: Partial<XiaobaoRiskSummaryPayload> = {}): XiaobaoRiskSummaryPayload {
  const riskLevel = patch.riskLevel ?? 'blocked';
  const riskScore = (patch.riskScore as number | undefined) ?? 88;
  return {
    versionId: 'version-1',
    versionName: 'V1.0',
    productName: 'FTB',
    projectName: '智能项目管理',
    expectedReleaseDate: '2026-07-09',
    forecastReleaseDate: '2026-07-12T08:00:00.000Z',
    delayDays: 3,
    remainingWorkHours: 24,
    riskLevel,
    riskScore,
    confidence: 80,
    riskSignature: patch.riskSignature ?? `version-1|${riskLevel}|${riskScore}`,
    signals: {
      unfinishedCount: 4,
      openBugCount: 2,
      criticalBugCount: 1,
      failedTestCount: 1,
      blockedCount: 1,
      silentRiskCount: 0,
      daysToExpectedRelease: 1,
    },
    reasons: [
      {
        key: 'critical_bug',
        title: '关键缺陷',
        severity: 'danger',
        detail: '仍有关键缺陷',
      },
    ],
    dailyEvidence: {
      todayDeliveries: [{ summary: '提交修复分支' }],
      todayProgress: [],
      todayCreations: [],
      todayRisks: [{ summary: '关键缺陷仍未关闭' }],
      progressNotes: [],
      needsProgressItems: [],
      recentActivityCount: 1,
      totalActivityCount: 4,
      todayActualHours: 2,
      lastActivityAt: '2026-07-08T07:00:00.000Z',
      silentRisks: [],
    },
    currentSnapshot: {
      versionId: 'version-1',
      date: '2026-07-08',
      riskScore,
      riskLevel,
      openBugCount: 2,
      criticalBugCount: 1,
      failedTestCount: 1,
      blockedCount: 1,
      silentRiskCount: 0,
      confidence: 80,
      createdAt: '2026-07-08T08:00:00.000Z',
    },
    ...patch,
  };
}
