import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { QuizService } from './quiz.service';
import { PrismaService } from '../../infrastructure/database/prisma.service';

describe('QuizService', () => {
  let service: QuizService;
  let prisma: any;

  beforeEach(async () => {
    prisma = {
      quiz: {
        findUnique: jest.fn(),
        findMany: jest.fn(),
        create: jest.fn(),
      },
      quizQuestion: {
        findMany: jest.fn(),
        createMany: jest.fn(),
        findUnique: jest.fn(),
      },
      quizAttempt: {
        findUnique: jest.fn(),
        findFirst: jest.fn(),
        findMany: jest.fn(),
        create: jest.fn(),
        update: jest.fn(),
        updateMany: jest.fn(),
      },
      quizAnswer: {
        create: jest.fn(),
        createMany: jest.fn(),
      },
      knowledgeBase: {
        findUnique: jest.fn(),
      },
      knowledgeItem: {
        findMany: jest.fn(),
      },
      // M-QUIZ-01-02: $transaction 将 callback 应用于 tx（共享同一 mock 引用）
      $transaction: jest.fn(),
    };

    // $transaction 在 prisma 赋值后绑定，确保捕获当前 mock 引用
    prisma.$transaction.mockImplementation((cb: Function) => cb({
      quizAttempt: { findUnique: prisma.quizAttempt.findUnique, update: prisma.quizAttempt.update, updateMany: prisma.quizAttempt.updateMany },
      quizQuestion: { findUnique: prisma.quizQuestion.findUnique },
      quizAnswer: { createMany: prisma.quizAnswer.createMany },
    }));

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        QuizService,
        { provide: PrismaService, useValue: prisma },
      ],
    }).compile();

    service = module.get(QuizService);
  });

  // ═══════════════════════════════════════════════
  // findOne
  // ═══════════════════════════════════════════════

  describe('findOne', () => {
    it('返回用户自己的 Quiz', async () => {
      prisma.quiz.findUnique.mockResolvedValue({ id: 'q1', userId: 'u1', title: 'Test' });
      const result = await service.findOne('u1', 'q1');
      expect(result).toEqual({ id: 'q1', userId: 'u1', title: 'Test' });
      // N3：userId 进入查询条件
      expect(prisma.quiz.findUnique).toHaveBeenCalledWith({ where: { id: 'q1', userId: 'u1' } });
    });

    it('不存在的 Quiz → NotFoundException', async () => {
      prisma.quiz.findUnique.mockResolvedValue(null);
      await expect(service.findOne('u1', 'nx')).rejects.toThrow(NotFoundException);
    });

    it('用户 B 查用户 A 的 Quiz → NotFoundException（N3：统一 404）', async () => {
      prisma.quiz.findUnique.mockResolvedValue(null);
      await expect(service.findOne('uB', 'q-of-user-a')).rejects.toThrow(NotFoundException);
    });
  });

  // ═══════════════════════════════════════════════
  // start
  // ═══════════════════════════════════════════════

  describe('start', () => {
    it('创建独立 Attempt', async () => {
      prisma.quiz.findUnique.mockResolvedValue({ id: 'q1', userId: 'u1', questionCount: 5 });
      prisma.quizAttempt.create.mockResolvedValue({ id: 'a1', quizId: 'q1', userId: 'u1', totalQuestions: 5 });

      const result = await service.start('u1', 'q1');
      expect(result.id).toBe('a1');
      // N3：userId 进入查询条件
      expect(prisma.quiz.findUnique).toHaveBeenCalledWith({ where: { id: 'q1', userId: 'u1' } });
      expect(prisma.quizAttempt.create).toHaveBeenCalledWith({
        data: { quizId: 'q1', userId: 'u1', totalQuestions: 5 },
      });
    });

    it('不存在的 Quiz → NotFoundException', async () => {
      prisma.quiz.findUnique.mockResolvedValue(null);
      await expect(service.start('u1', 'nx')).rejects.toThrow(NotFoundException);
    });

    it('用户 B 在用户 A 的 Quiz 上创建 Attempt → NotFoundException（N3：统一 404）', async () => {
      prisma.quiz.findUnique.mockResolvedValue(null);
      await expect(service.start('uB', 'q-of-user-a')).rejects.toThrow(NotFoundException);
    });
  });

  // ═══════════════════════════════════════════════
  // getAttemptQuestions
  // ═══════════════════════════════════════════════

  describe('getAttemptQuestions', () => {
    const fullQuestions = [
      { id: 'qq1', type: 'choice', stem: 'Q1?', options: ['A', 'B'], answer: '0', explanation: 'Because A', sourceBlockIds: null, orderIndex: 0 },
      { id: 'qq2', type: 'fill', stem: 'Q2?', options: null, answer: 'key', explanation: 'Because B', sourceBlockIds: null, orderIndex: 1 },
    ];

    // Mock findMany to simulate Prisma select behavior
    function mockFindMany() {
      prisma.quizQuestion.findMany.mockImplementation((args: any) => {
        const select = args?.select ?? {};
        return fullQuestions.map(q => {
          const filtered: any = {};
          for (const key of Object.keys(select)) {
            if (select[key] === true) filtered[key] = (q as any)[key];
          }
          return filtered;
        });
      });
    }

    beforeEach(() => {
      mockFindMany();
    });

    it('未提交 Attempt → 不返回 answer/explanation', async () => {
      prisma.quizAttempt.findUnique.mockResolvedValue({
        id: 'a1', quizId: 'q1', finishedAt: null,
      });

      const result = await service.getAttemptQuestions('u1', 'a1');

      for (const q of result) {
        expect(q.answer).toBeUndefined();
        expect(q.explanation).toBeUndefined();
      }
      // N2：userId 进入查询条件
      expect(prisma.quizAttempt.findUnique).toHaveBeenCalledWith({
        where: { id: 'a1', userId: 'u1' },
        select: { id: true, quizId: true, finishedAt: true },
      });
      const findManyCall = prisma.quizQuestion.findMany.mock.calls[0][0];
      expect(findManyCall.select.answer).toBeUndefined();
      expect(findManyCall.select.explanation).toBeUndefined();
    });

    it('已提交 Attempt → 返回 answer/explanation', async () => {
      prisma.quizAttempt.findUnique.mockResolvedValue({
        id: 'a1', quizId: 'q1', finishedAt: new Date(),
      });

      const result = await service.getAttemptQuestions('u1', 'a1');

      for (const q of result) {
        expect(q.answer).toBeDefined();
        expect(q.explanation).toBeDefined();
      }
      const findManyCall = prisma.quizQuestion.findMany.mock.calls[0][0];
      expect(findManyCall.select.answer).toBe(true);
      expect(findManyCall.select.explanation).toBe(true);
    });

    it('不存在的 Attempt → NotFoundException', async () => {
      prisma.quizAttempt.findUnique.mockResolvedValue(null);
      await expect(service.getAttemptQuestions('u1', 'nx')).rejects.toThrow(NotFoundException);
    });

    it('跨用户 Attempt → NotFoundException（N2：统一 404）', async () => {
      prisma.quizAttempt.findUnique.mockResolvedValue(null);
      await expect(service.getAttemptQuestions('uB', 'a-of-user-a')).rejects.toThrow(NotFoundException);
    });

    it('历史 Attempt 完成不影响当前未提交 Attempt 答案可见性', async () => {
      // 模拟：用户有一个已完成的历史 Attempt a-old，当前正在做新 Attempt a-new
      // getAttemptQuestions('u1', 'a-new') 应该按 a-new 的状态判断
      prisma.quizAttempt.findUnique.mockResolvedValue({
        id: 'a-new', quizId: 'q1', finishedAt: null,
      });

      const result = await service.getAttemptQuestions('u1', 'a-new');

      // a-new 未完成 → 不返回答案
      for (const q of result) {
        expect(q.answer).toBeUndefined();
        expect(q.explanation).toBeUndefined();
      }
    });
  });

  // ═══════════════════════════════════════════════
  // getResults
  // ═══════════════════════════════════════════════

  describe('getResults', () => {
    it('返回用户自己的已完成 Attempt 结果（含答案）', async () => {
      const attemptWithAnswers = {
        id: 'a1', quizId: 'q1', userId: 'u1', finishedAt: new Date(),
        answers: [{ id: 'ans1', questionId: 'qq1', userAnswer: '0', isCorrect: true, question: { id: 'qq1', type: 'choice', answer: '0', explanation: 'Because' } }],
      };
      prisma.quizAttempt.findUnique.mockResolvedValue(attemptWithAnswers);

      const result = await service.getResults('u1', 'a1');
      expect(result).toEqual(attemptWithAnswers);
      // N1 修复：userId 进入查询条件
      expect(prisma.quizAttempt.findUnique).toHaveBeenCalledWith({
        where: { id: 'a1', userId: 'u1' },
        include: { answers: { include: { question: true } } },
      });
    });

    it('不存在的 Attempt → NotFoundException（不暴露存在性）', async () => {
      prisma.quizAttempt.findUnique.mockResolvedValue(null);
      await expect(service.getResults('u1', 'nx')).rejects.toThrow(NotFoundException);
    });

    it('用户 B 查用户 A 的 Attempt → NotFoundException（N1：不暴露存在性）', async () => {
      // N1 修复：userId 进入查询条件后，跨用户返回 404 而非 403
      prisma.quizAttempt.findUnique.mockResolvedValue(null);
      await expect(service.getResults('uB', 'a-of-user-a')).rejects.toThrow(NotFoundException);
    });
  });

  // ═══════════════════════════════════════════════
  // submit（M-QUIZ-01-02 + N1：updateMany CAS 原子提交）
  // ═══════════════════════════════════════════════

  describe('submit', () => {
    it('空答案数组 → BadRequestException', async () => {
      await expect(service.submit('u1', 'a1', [])).rejects.toThrow('至少需要提交一个答案');
    });

    it('服务端判分：正确和错误答案分别计分', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 }); // CAS success
      prisma.quizQuestion.findUnique
        .mockResolvedValueOnce({ id: 'qq1', type: 'choice', answer: '0' })
        .mockResolvedValueOnce({ id: 'qq2', type: 'fill', answer: 'key' });
      prisma.quizAnswer.createMany.mockResolvedValue({ count: 2 });
      prisma.quizAttempt.update.mockResolvedValue({});

      const result = await service.submit('u1', 'a1', [
        { questionId: 'qq1', answer: '0' },   // correct
        { questionId: 'qq2', answer: 'wrong' }, // incorrect
      ]);

      expect(result.score).toBe(50);
      expect(result.correctCount).toBe(1);
      // N1：CAS 先占位
      expect(prisma.quizAttempt.updateMany).toHaveBeenCalledWith({
        where: { id: 'a1', userId: 'u1', finishedAt: null },
        data: { finishedAt: expect.any(Date) },
      });
      // 服务端判分
      expect(prisma.quizAnswer.createMany).toHaveBeenCalledWith({
        data: [
          { attemptId: 'a1', questionId: 'qq1', userAnswer: '0', isCorrect: true },
          { attemptId: 'a1', questionId: 'qq2', userAnswer: 'wrong', isCorrect: false },
        ],
      });
    });

    it('fill 规范化判分：空白/大小写差异判对', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 });
      prisma.quizQuestion.findUnique.mockResolvedValue({ id: 'qq1', type: 'fill', answer: '氧气' });
      prisma.quizAnswer.createMany.mockResolvedValue({ count: 1 });
      prisma.quizAttempt.update.mockResolvedValue({});

      const result = await service.submit('u1', 'a1', [{ questionId: 'qq1', answer: '  氧气  ' }]);
      expect(result.correctCount).toBe(1);
      expect(prisma.quizAnswer.createMany).toHaveBeenCalledWith({
        data: [{ attemptId: 'a1', questionId: 'qq1', userAnswer: '  氧气  ', isCorrect: true }],
      });
    });

    it('非法 question type → fail-closed', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 });
      prisma.quizQuestion.findUnique.mockResolvedValue({ id: 'qq1', type: 'essay', answer: '...' });

      await expect(service.submit('u1', 'a1', [{ questionId: 'qq1', answer: 'x' }]))
        .rejects.toThrow('题目 qq1 类型非法');
    });

    it('题目不存在 → BadRequestException（事务回滚）', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 }); // CAS success
      prisma.quizQuestion.findUnique.mockResolvedValue(null);

      await expect(service.submit('u1', 'a1', [{ questionId: 'nx', answer: '0' }]))
        .rejects.toThrow('题目 nx 不存在');
      // Answer 不应被写入
      expect(prisma.quizAnswer.createMany).not.toHaveBeenCalled();
    });

    it('跨用户 Attempt → NotFoundException', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 0 }); // CAS fail
      prisma.quizAttempt.findUnique.mockResolvedValue(null);

      await expect(service.submit('uB', 'a-of-user-a', [{ questionId: 'qq1', answer: '0' }]))
        .rejects.toThrow(NotFoundException);
    });

    it('已提交 Attempt → BadRequestException（CAS 失败）', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 0 }); // CAS fail
      prisma.quizAttempt.findUnique.mockResolvedValue({ id: 'a1', quizId: 'q1', userId: 'u1', finishedAt: new Date() });

      await expect(service.submit('u1', 'a1', [{ questionId: 'qq1', answer: '0' }]))
        .rejects.toThrow('已提交过答案');
    });

    it('N1：并发提交只有一个能通过 CAS', async () => {
      // 模拟第一个事务成功
      prisma.quizAttempt.updateMany.mockResolvedValueOnce({ count: 1 });
      prisma.quizQuestion.findUnique.mockResolvedValue({ id: 'qq1', type: 'choice', answer: '0' });
      prisma.quizAnswer.createMany.mockResolvedValue({ count: 1 });
      prisma.quizAttempt.update.mockResolvedValue({});

      await service.submit('u1', 'a1', [{ questionId: 'qq1', answer: '0' }]);

      // CAS 以 finishedAt: null 为条件
      expect(prisma.quizAttempt.updateMany).toHaveBeenCalledWith({
        where: { id: 'a1', userId: 'u1', finishedAt: null },
        data: { finishedAt: expect.any(Date) },
      });
    });

    it('原子提交：Answer 写入与 Attempt 结算在同一事务', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 });
      prisma.quizQuestion.findUnique.mockResolvedValue({ id: 'qq1', type: 'choice', answer: '0' });
      prisma.quizAnswer.createMany.mockResolvedValue({ count: 1 });
      prisma.quizAttempt.update.mockResolvedValue({});

      await service.submit('u1', 'a1', [{ questionId: 'qq1', answer: '0' }]);

      expect(prisma.$transaction).toHaveBeenCalledTimes(1);
      expect(prisma.quizAnswer.createMany).toHaveBeenCalled();
      // 最终 update 只设 score/correctCount（finishedAt 已由 CAS 占位）
      expect(prisma.quizAttempt.update).toHaveBeenCalledWith({
        where: { id: 'a1' },
        data: { correctCount: 1, score: 100 },
      });
    });

    it('部分失败无脏数据：createMany 失败时事务回滚', async () => {
      prisma.quizAttempt.updateMany.mockResolvedValue({ count: 1 });
      prisma.quizQuestion.findUnique.mockResolvedValue({ id: 'qq1', type: 'choice', answer: '0' });
      prisma.quizAnswer.createMany.mockRejectedValue(new Error('DB write error'));

      await expect(service.submit('u1', 'a1', [{ questionId: 'qq1', answer: '0' }]))
        .rejects.toThrow('DB write error');

      expect(prisma.quizAttempt.update).not.toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════
  // getHistory
  // ═══════════════════════════════════════════════

  describe('getHistory', () => {
    it('返回用户自己的 Attempt 历史', async () => {
      prisma.quizAttempt.findMany.mockResolvedValue([{ id: 'a1', quizId: 'q1' }]);
      const result = await service.getHistory('u1');
      expect(result).toEqual([{ id: 'a1', quizId: 'q1' }]);
      expect(prisma.quizAttempt.findMany).toHaveBeenCalledWith({
        where: { userId: 'u1' }, orderBy: { startedAt: 'desc' }, take: 50,
        include: { quiz: { select: { title: true } } },
      });
    });
  });
});
