import { QuizGenerationProjector } from './quiz-generation-projector';
import type { ProjectionContext } from './result-projector.interface';

function makeTx(): any {
  const store: Record<string, any[]> = {
    aiJobArtifact: [],
    quiz: [],
    quizQuestion: [],
  };
  return {
    aiJobArtifact: {
      findMany: jest.fn(async (args: any) =>
        store.aiJobArtifact.filter(a => a.jobId === args.where.jobId)),
      create: jest.fn(async (args: any) => {
        const dup = store.aiJobArtifact.find(
          a => a.jobId === args.data.jobId && a.artifactType === args.data.artifactType && a.artifactId === args.data.artifactId,
        );
        if (dup) { const e: any = new Error('P2002'); e.code = 'P2002'; throw e; }
        store.aiJobArtifact.push(args.data);
        return args.data;
      }),
    },
    quiz: {
      create: jest.fn(async (args: any) => {
        const q = { id: `qz_${store.quiz.length + 1}`, ...args.data };
        store.quiz.push(q);
        return q;
      }),
    },
    quizQuestion: {
      create: jest.fn(async (args: any) => {
        const qq = { id: `qq_${store.quizQuestion.length + 1}`, ...args.data };
        store.quizQuestion.push(qq);
        return qq;
      }),
    },
  };
}

function makeCtx(overrides?: any): ProjectionContext {
  return {
    job: { id: 'job-001', userId: 'u-001', jobType: 'quiz_generation', targetType: 'knowledge_base', targetId: 'kb-001', snapshotId: 'snap-001', promptVersion: '1.0.0', outputSchemaVersion: 'quiz-generation-v1' },
    snapshot: { snapshot: { knowledgeBaseId: 'kb-001' } },
    validatedOutput: {
      quizTitle: 'Test Quiz',
      quizDescription: 'A test',
      questions: [
        { type: 'choice', stem: 'Q1?', options: ['A', 'B', 'C', 'D'], answer: '0', explanation: 'Because A', sourceBlockIds: ['ki-1'] },
        { type: 'judge', stem: 'Q2?', answer: 'true', explanation: 'Correct' },
        { type: 'fill', stem: 'Q3: fill the blank', answer: '42', explanation: 'Answer is 42' },
      ],
    },
    ...overrides,
  };
}

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

  beforeEach(() => { projector = new QuizGenerationProjector(); });

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

  it('创建 Quiz + Questions + Artifact', async () => {
    const tx = makeTx();
    const artifacts = await projector.project(tx, makeCtx());

    expect(artifacts).toHaveLength(1); // 1 Quiz artifact
    expect(artifacts[0].artifactType).toBe('Quiz');
    expect(tx.quiz.create).toHaveBeenCalledTimes(1);
    expect(tx.quizQuestion.create).toHaveBeenCalledTimes(3);

    // Quiz fields
    const quizCall = tx.quiz.create.mock.calls[0][0].data;
    expect(quizCall.userId).toBe('u-001');
    expect(quizCall.knowledgeBaseId).toBe('kb-001');
    expect(quizCall.sourceType).toBe('ai');
    expect(quizCall.sourceId).toBe('job-001');
    expect(quizCall.status).toBe('ready');
    expect(quizCall.questionCount).toBe(3);

    // Question fields
    const qCall = tx.quizQuestion.create.mock.calls[0][0].data;
    expect(qCall.type).toBe('choice');
    expect(qCall.stem).toBe('Q1?');
    expect(qCall.options).toEqual(['A', 'B', 'C', 'D']);
    expect(qCall.answer).toBe('0');
    expect(qCall.explanation).toBe('Because A');
    expect(qCall.orderIndex).toBe(0);
  });

  it('入口幂等：已有 Artifact → 直接返回', async () => {
    const tx = makeTx();
    await projector.project(tx, makeCtx());
    const artifacts = await projector.project(tx, makeCtx());
    expect(artifacts).toHaveLength(1);
    // No additional creates
    expect(tx.quiz.create).toHaveBeenCalledTimes(1);
    expect(tx.quizQuestion.create).toHaveBeenCalledTimes(3);
  });

  it('P2002 → 静默跳过', async () => {
    const tx = makeTx();
    tx.aiJobArtifact.create = jest.fn(async () => { const e: any = new Error('P2002'); e.code = 'P2002'; throw e; });
    tx.aiJobArtifact.findMany = jest.fn().mockResolvedValue([]);
    const artifacts = await projector.project(tx, makeCtx());
    expect(artifacts).toHaveLength(1); // Quiz still created, artifact ref returned
    expect(tx.quiz.create).toHaveBeenCalledTimes(1);
  });

  it('空 questions → 不创建 Quiz', async () => {
    const tx = makeTx();
    const artifacts = await projector.project(tx, makeCtx({ validatedOutput: { quizTitle: 'T', questions: [] } }));
    expect(artifacts).toHaveLength(0);
    expect(tx.quiz.create).not.toHaveBeenCalled();
  });

  it('questions 中无效项被过滤（缺少 stem/answer）', async () => {
    const tx = makeTx();
    const ctx = makeCtx({
      validatedOutput: {
        quizTitle: 'T',
        questions: [
          { stem: 'Valid', answer: 'A', type: 'fill', explanation: 'E' },
          { stem: '', answer: 'B', type: 'fill', explanation: 'E' },  // no stem
          { answer: 'C', type: 'fill', explanation: 'E' },             // no stem
        ],
      },
    });
    const artifacts = await projector.project(tx, ctx);
    expect(artifacts).toHaveLength(1);
    expect(tx.quizQuestion.create).toHaveBeenCalledTimes(1);
  });

  it('fill 题型：options 为 undefined', async () => {
    const tx = makeTx();
    const ctx = makeCtx({
      validatedOutput: {
        quizTitle: 'T',
        questions: [{ type: 'fill', stem: 'Q', answer: 'A', explanation: 'E' }],
      },
    });
    await projector.project(tx, ctx);
    const qCall = tx.quizQuestion.create.mock.calls[0][0].data;
    expect(qCall.options).toBeUndefined();
  });

  // ═══════════════════════════════════════════════
  // M-QUIZ-01-CLEANUP-01: 非法 type fail-closed
  // ═══════════════════════════════════════════════

  it('单个非法 type → 拒绝整个 Quiz（0 Quiz, 0 Question, 0 Artifact）', async () => {
    const tx = makeTx();
    const ctx = makeCtx({
      validatedOutput: {
        quizTitle: 'Bad Quiz',
        questions: [
          { type: 'choice', stem: 'Q1', answer: '0', explanation: 'E', options: ['A', 'B'] },
          { type: 'essay', stem: 'Q2', answer: 'long', explanation: 'E' },
          { type: 'judge', stem: 'Q3', answer: 'true', explanation: 'E' },
        ],
      },
    });

    await expect(projector.project(tx, ctx)).rejects.toThrow('QUIZ_INVALID_QUESTION_TYPE');

    // 原子回滚：Quiz 未创建、Question 未创建
    expect(tx.quiz.create).not.toHaveBeenCalled();
    expect(tx.quizQuestion.create).not.toHaveBeenCalled();
  });

  it('多个非法 type → 拒绝，错误消息列出所有位置', async () => {
    const tx = makeTx();
    const ctx = makeCtx({
      validatedOutput: {
        quizTitle: 'Bad Quiz',
        questions: [
          { type: 'multi_choice', stem: 'Q1', answer: '0', explanation: 'E', options: ['A', 'B'] },
          { type: 'short_answer', stem: 'Q2', answer: 'x', explanation: 'E' },
        ],
      },
    });

    await expect(projector.project(tx, ctx)).rejects.toThrow(/QUIZ_INVALID_QUESTION_TYPE.*2 question/);
  });

  it('合法 choice+fill+judge 正常通过', async () => {
    const tx = makeTx();
    const ctx = makeCtx({
      validatedOutput: {
        quizTitle: 'Good Quiz',
        questions: [
          { type: 'choice', stem: 'Q1', answer: '0', explanation: 'E', options: ['A', 'B'] },
          { type: 'fill', stem: 'Q2', answer: 'x', explanation: 'E' },
          { type: 'judge', stem: 'Q3', answer: 'true', explanation: 'E' },
        ],
      },
    });

    const artifacts = await projector.project(tx, ctx);
    expect(artifacts).toHaveLength(1);
    expect(tx.quiz.create).toHaveBeenCalledTimes(1);
    expect(tx.quizQuestion.create).toHaveBeenCalledTimes(3);
  });
});
