import { Test, TestingModule } from '@nestjs/testing';
import type { Prisma } from '@prisma/client';
import { ActiveRecallProjector } from './active-recall-projector';
import { ProjectionContext } from './result-projector.interface';

// ═══════════════════════════════════════════════════════════════════════════
// Helpers
// ═══════════════════════════════════════════════════════════════════════════

function makeContext(overrides?: Partial<ProjectionContext['job']>): ProjectionContext {
  return {
    job: {
      id: 'job-001',
      userId: 'u-001',
      jobType: 'active_recall',
      targetType: 'active_recall_answer',
      targetId: 'a-001',
      snapshotId: 'snap-001',
      promptVersion: '1.0.0',
      outputSchemaVersion: 'active-recall-v1',
      ...overrides,
    },
    snapshot: { snapshot: { userId: 'u-001', answerId: 'a-001' } },
    validatedOutput: {
      score: 72,
      masteryLevel: 'partial',
      summary: '用户理解了核心概念。',
      strengths: ['核心概念正确'],
      weaknesses: ['缺少例子', '遗漏条件'],
      missingKeyPoints: ['关键点1'],
      misconceptions: [],
      weaknessTypes: ['missing_detail'],
      focusItems: [
        { title: 'X的应用条件', reason: '未提及', suggestion: '回顾知识点', priority: 'high' },
      ],
      reviewSuggestion: {
        shouldReview: true,
        intervalDays: 2,
        cardFront: 'X的应用条件是什么？',
        cardBack: '当Y存在时X失效。',
      },
    },
  };
}

function createMockTx() {
  type ArtifactRef = { artifactType: string; artifactId: string; ordinal: number };
  const store: Record<string, any[]> = {
    aiAnalysisResult: [],
    focusItem: [],
    reviewCard: [],
    aiJobArtifact: [],
  };

  return {
    store,
    aiAnalysisResult: {
      upsert: jest.fn(async (args: any) => {
        const idx = store.aiAnalysisResult.findIndex((r: any) => r.id === args.where.id);
        if (idx >= 0) {
          store.aiAnalysisResult[idx] = { ...store.aiAnalysisResult[idx], ...args.create, ...args.update };
          return store.aiAnalysisResult[idx];
        }
        const record = { ...args.create };
        store.aiAnalysisResult.push(record);
        return record;
      }),
    },
    focusItem: {
      findFirst: jest.fn(async () => null),
      create: jest.fn(async (args: any) => {
        const record = { id: `fi-${store.focusItem.length + 1}`, ...args.data };
        store.focusItem.push(record);
        return record;
      }),
    },
    reviewCard: {
      findFirst: jest.fn(async () => null),
      create: jest.fn(async (args: any) => {
        const record = { id: `rc-${store.reviewCard.length + 1}`, ...args.data };
        store.reviewCard.push(record);
        return record;
      }),
    },
    aiJobArtifact: {
      findMany: jest.fn<Promise<ArtifactRef[]>, [unknown]>(async () => []),
      create: jest.fn(async (args: any) => {
        const record = { ...args.data };
        store.aiJobArtifact.push(record);
        return record;
      }),
    },
  };
}

type PrismaTx = Prisma.TransactionClient;

// ═══════════════════════════════════════════════════════════════════════════
// Tests
// ═══════════════════════════════════════════════════════════════════════════

describe('ActiveRecallProjector', () => {
  let projector: ActiveRecallProjector;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [ActiveRecallProjector],
    }).compile();

    projector = module.get(ActiveRecallProjector);
  });

  it('key is active_recall_projector', () => {
    expect(projector.key).toBe('active_recall_projector');
  });

  describe('事务原子性', () => {
    it('正常投影创建所有 Artifact', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);

      // AiAnalysisResult + 1 FocusItem (from focusItems) + 1 ReviewCard = 3 artifacts
      expect(artifacts.length).toBe(3);
      expect(tx.store.aiAnalysisResult.length).toBe(1);
      expect(tx.store.focusItem.length).toBe(1);
      expect(tx.store.reviewCard.length).toBe(1);
      expect(tx.store.aiJobArtifact.length).toBe(3);

      // Artifact type check
      const types = artifacts.map((a) => a.artifactType);
      expect(types).toContain('AiAnalysisResult');
      expect(types).toContain('FocusItem');
      expect(types).toContain('ReviewCard');
    });

    it('AiAnalysisResult 写入失败 → tx 回滚（无部分结果）', async () => {
      const tx = createMockTx();
      tx.aiAnalysisResult.upsert.mockRejectedValue(new Error('DB error'));
      const ctx = makeContext();

      await expect(projector.project(tx as unknown as PrismaTx, ctx)).rejects.toThrow('DB error');

      // 后续写入不应发生
      expect(tx.store.focusItem.length).toBe(0);
      expect(tx.store.reviewCard.length).toBe(0);
    });

    it('FocusItem 创建失败 → tx 回滚', async () => {
      const tx = createMockTx();
      tx.focusItem.create.mockRejectedValue(new Error('FocusItem insert failed'));
      const ctx = makeContext();

      await expect(projector.project(tx as unknown as PrismaTx, ctx)).rejects.toThrow('FocusItem insert failed');

      // AiAnalysisResult 已写入但 FocusItem 失败 → 依赖 tx 回滚
      // (实际 Prisma tx 会回滚，这里验证异常传播)
    });

    it('无 focusItems 时不创建 FocusItem', async () => {
      const tx = createMockTx();
      const ctx = makeContext();
      ctx.validatedOutput.focusItems = [];

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);

      expect(tx.store.focusItem.length).toBe(0);
      expect(artifacts.filter((a) => a.artifactType === 'FocusItem').length).toBe(0);
    });

    it('reviewSuggestion.shouldReview=false 时不创建 ReviewCard', async () => {
      const tx = createMockTx();
      const ctx = makeContext();
      ctx.validatedOutput.reviewSuggestion = { shouldReview: false };

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);

      expect(tx.store.reviewCard.length).toBe(0);
      expect(artifacts.filter((a) => a.artifactType === 'ReviewCard').length).toBe(0);
    });
  });

  describe('幂等性', () => {
    it('重复执行不重复创建实体', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      // 第一次执行
      const a1 = await projector.project(tx as unknown as PrismaTx, ctx);
      expect(a1.length).toBe(3);

      // 第二次执行（同一 jobId）
      // 模拟已有 Artifact（SyntheticResultProjector 模式）
      tx.aiJobArtifact.findMany.mockImplementation(async () =>
        tx.store.aiJobArtifact.map((a) => ({ ...a })),
      );

      const a2 = await projector.project(tx as unknown as PrismaTx, ctx);
      expect(a2.length).toBe(3); // 返回已有引用，不重复创建
      // 未调用 create/upsert（直接返回已有 artifacts）
    });

    it('已 succeeded 的 Job 直接返回已有 Artifact', async () => {
      const tx = createMockTx();
      // 模拟已有 Artifact
      const existingArtifacts = [
        { artifactType: 'AiAnalysisResult', artifactId: 'ar_existing', ordinal: 0 },
      ];
      tx.aiJobArtifact.findMany.mockImplementation(async () => existingArtifacts);
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);

      expect(artifacts.length).toBe(1);
      expect(artifacts[0].artifactId).toBe('ar_existing');
      // 不创建新实体
    });

    it('AiAnalysisResult upsert 幂等', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      // 第一次
      await projector.project(tx as unknown as PrismaTx, ctx);
      const firstResultId = tx.store.aiAnalysisResult[0].id;

      // 第二次（重置 mock 返回空 artifact → 触发重新投影）
      tx.aiJobArtifact.findMany.mockImplementation(async () => []);
      // 但 upsert 找到已有记录 → update
      await projector.project(tx as unknown as PrismaTx, ctx);

      // 仍只有 1 条 AiAnalysisResult（upsert，非重复插入）
      expect(tx.store.aiAnalysisResult.length).toBe(1);
      expect(tx.store.aiAnalysisResult[0].id).toBe(firstResultId);
    });

    it('AiJobArtifact @unique([jobId, artifactType, artifactId]) 防重复', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      // 第一次写入 artifact
      await projector.project(tx as unknown as PrismaTx, ctx);
      expect(tx.aiJobArtifact.create).toHaveBeenCalled();

      // 第二次 — P2002 冲突 → 幂等跳过
      tx.aiJobArtifact.findMany.mockImplementation(async () => []);
      tx.aiJobArtifact.create.mockRejectedValue({ code: 'P2002' });

      // 不应抛出（P2002 被 catch）
      // Artifact 创建失败不阻止其他写入（但 AiAnalysisResult upsert 会继续）
      // 注意：实际 tx 中 P2002 会被 upsertArtifact catch
    });
  });

  describe('Artifact 可反向查询', () => {
    it('每个 Artifact 包含 artifactType + artifactId + ordinal', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);

      for (const a of artifacts) {
        expect(a.artifactType).toBeTruthy();
        expect(a.artifactId).toBeTruthy();
        expect(typeof a.ordinal).toBe('number');
      }
    });

    it('AiAnalysisResult artifactId 为确定性 ID', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);
      const arArtifact = artifacts.find((a) => a.artifactType === 'AiAnalysisResult');

      expect(arArtifact).toBeDefined();
      expect(arArtifact!.artifactId).toBe(`ar_${ctx.job.id.substring(0, 24)}`);
    });

    it('FocusItem artifactId 可关联到实际 FocusItem', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);
      const fiArtifacts = artifacts.filter((a) => a.artifactType === 'FocusItem');

      expect(fiArtifacts.length).toBe(1);
      for (const fa of fiArtifacts) {
        const fi = tx.store.focusItem.find((f: any) => f.id === fa.artifactId);
        expect(fi).toBeDefined();
      }
    });
  });

  describe('AiAnalysisResult 内容', () => {
    it('字段映射与旧链路一致', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      await projector.project(tx as unknown as PrismaTx, ctx);
      const result = tx.store.aiAnalysisResult[0];

      expect(result.userId).toBe('u-001');
      expect(result.jobId).toBe('job-001');
      expect(result.summary).toBe('用户理解了核心概念。');
      expect(result.masteryScore).toBe(72);
      expect(result.strengths).toEqual(['核心概念正确']);
      expect(result.weaknesses).toEqual(['缺少例子', '遗漏条件']);
      expect(result.suggestions).toEqual(ctx.validatedOutput.focusItems);
      expect(result.rawResult).toEqual(ctx.validatedOutput);
    });

    it('score 为 null 时正常写入', async () => {
      const tx = createMockTx();
      const ctx = makeContext();
      delete ctx.validatedOutput.score;

      await projector.project(tx as unknown as PrismaTx, ctx);
      expect(tx.store.aiAnalysisResult[0].masteryScore).toBeNull();
    });
  });

  describe('FocusItem 内容', () => {
    it('reason/suggestion 从 AI 输出写入', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      await projector.project(tx as unknown as PrismaTx, ctx);
      const fi = tx.store.focusItem[0];

      expect(fi.title).toBe('X的应用条件');
      expect(fi.reason).toBe('未提及');
      expect(fi.suggestion).toBe('回顾知识点');
      expect(fi.priority).toBe('high');
    });
  });

  describe('ReviewCard 内容', () => {
    it('从 reviewSuggestion 创建 ReviewCard', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      await projector.project(tx as unknown as PrismaTx, ctx);
      const card = tx.store.reviewCard[0];

      expect(card.userId).toBe('u-001');
      expect(card.frontText).toBe('X的应用条件是什么？');
      expect(card.backText).toBe('当Y存在时X失效。');
      expect(card.intervalDays).toBe(2);
      expect(card.status).toBe('active');
      expect(card.scheduleState).toBe('new');
    });

    it('intervalDays 限制在 [1, 365]', async () => {
      const tx = createMockTx();
      const ctx = makeContext();
      ctx.validatedOutput.reviewSuggestion.intervalDays = 400;

      await projector.project(tx as unknown as PrismaTx, ctx);
      const card = tx.store.reviewCard[0];
      expect(card.intervalDays).toBe(365);
    });
  });

  describe('ordinal 顺序', () => {
    it('ordinal 递增不重复', async () => {
      const tx = createMockTx();
      const ctx = makeContext();

      const artifacts = await projector.project(tx as unknown as PrismaTx, ctx);
      const ordinals = artifacts.map((a) => a.ordinal);
      const uniqueOrdinals = [...new Set(ordinals)];

      expect(ordinals.length).toBe(uniqueOrdinals.length); // 无重复
      expect(ordinals).toEqual([0, 1, 2]); // 递增
    });
  });
});
