import { Test, TestingModule } from '@nestjs/testing';
import { ForbiddenException } from '@nestjs/common';
import { LearningAnalysisSnapshotBuilder } from './learning-analysis-snapshot-builder';
import { LEARNING_ANALYSIS_JOB_DEFINITION } from './learning-analysis-job-definition';
import { JobDefinitionRegistry } from './job-definition-registry';
import { PrismaService } from '../../infrastructure/database/prisma.service';

describe('LearningAnalysisSnapshotBuilder', () => {
  let builder: LearningAnalysisSnapshotBuilder;
  let prisma: any;
  let registry: any;

  beforeEach(async () => {
    prisma = {
      user: { findUnique: jest.fn().mockResolvedValue({ id: 'u-001' }) },
      userLearningProfile: { findUnique: jest.fn().mockResolvedValue({ userId: 'u-001', learningGoal: 'mastery', qualityPreference: 'standard', dailyAvailableMinutes: 60 }) },
      dailyLearningActivity: { findMany: jest.fn().mockResolvedValue([{ durationSeconds: 1800, activeRecallCount: 3, reviewCount: 5, readingSeconds: 600 }]) },
      reviewCard: { count: jest.fn().mockResolvedValue(10) },
      reviewLog: { count: jest.fn().mockResolvedValue(15), findMany: jest.fn().mockResolvedValue([{ rating: 4 }, { rating: 3 }, { rating: 5 }]) },
      quiz: { count: jest.fn().mockResolvedValue(2) },
      quizAttempt: { findMany: jest.fn().mockResolvedValue([{ correctCount: 3, totalQuestions: 5 }, { correctCount: 4, totalQuestions: 5 }]) },
      quizAnswer: {
        findMany: jest.fn().mockResolvedValue([
          { isCorrect: true,  question: { type: 'choice' } },
          { isCorrect: true,  question: { type: 'choice' } },
          { isCorrect: false, question: { type: 'choice' } },
          { isCorrect: true,  question: { type: 'fill' } },
          { isCorrect: false, question: { type: 'fill' } },
          { isCorrect: true,  question: { type: 'judge' } },
        ]),
      },
      aiAnalysisResult: { findMany: jest.fn().mockResolvedValue([{ masteryScore: 75, weaknesses: ['w1', 'w2'] }]) },
      focusItem: { count: jest.fn().mockResolvedValue(2), findMany: jest.fn().mockResolvedValue([{ knowledgeItemId: 'ki-1' }]) },
      knowledgeItem: { count: jest.fn().mockResolvedValue(20), findMany: jest.fn().mockResolvedValue([{ id: 'ki-1', title: 'Weak Item' }]) },
    };
    registry = { get: jest.fn().mockReturnValue(LEARNING_ANALYSIS_JOB_DEFINITION) };

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

    builder = module.get(LearningAnalysisSnapshotBuilder);
    jest.spyOn(require('@nestjs/common').Logger.prototype, 'log').mockImplementation(() => {});
  });

  describe('build', () => {
    it('构建完整快照', async () => {
      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });

      expect(snapshot.schemaVersion).toBe('learning-analysis-v1');
      expect(snapshot.snapshot.userId).toBe('u-001');
      expect(snapshot.snapshot.triggerType).toBe('manual');
      expect(snapshot.snapshot.studyMetrics.activeDays).toBe(1);
      expect(snapshot.snapshot.studyMetrics.totalStudyDuration).toBe(1800);
      expect(snapshot.snapshot.reviewMetrics.reviewAccuracy).toBeGreaterThan(0);
      expect(snapshot.snapshot.quizMetrics.quizCount).toBe(2);
      expect(snapshot.snapshot.activeRecallMetrics.count).toBe(1);
      expect(snapshot.snapshot.feynmanMetrics.focusItemCount).toBe(2);
      expect(snapshot.snapshot.knowledgeProgress.weakItems).toHaveLength(1);
    });

    it('prompt/model 值来自 Definition（单一事实来源）', async () => {
      registry.get.mockReturnValue({ ...LEARNING_ANALYSIS_JOB_DEFINITION, prompt: { promptKey: 'custom-la', promptVersion: '2.0' } });
      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      expect(snapshot.snapshot.promptKey).toBe('custom-la');
      expect(snapshot.snapshot.promptVersion).toBe('2.0');
    });

    it('User 不存在 → ForbiddenException', async () => {
      prisma.user.findUnique.mockResolvedValue(null);
      await expect(builder.build({ userId: 'u-missing', triggerType: 'manual', operationId: 'op-001' })).rejects.toThrow(ForbiddenException);
    });

    it('无数据时 dataQuality 标记 insufficient', async () => {
      prisma.dailyLearningActivity.findMany.mockResolvedValue([]);
      prisma.reviewLog.findMany.mockResolvedValue([]);
      prisma.reviewLog.count.mockResolvedValue(0);
      prisma.reviewCard.count.mockResolvedValue(0);
      prisma.quizAttempt.findMany.mockResolvedValue([]);
      prisma.quiz.count.mockResolvedValue(0);
      prisma.aiAnalysisResult.findMany.mockResolvedValue([]);
      prisma.focusItem.count.mockResolvedValue(0);
      prisma.focusItem.findMany.mockResolvedValue([]);
      prisma.knowledgeItem.count.mockResolvedValue(0);
      prisma.knowledgeItem.findMany.mockResolvedValue([]);

      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      expect(snapshot.snapshot.dataQuality.overall).toBe('insufficient');
      expect(snapshot.snapshot.dataQuality.insufficientDataReasons).toContain('no_data_in_window');
    });

    it('M-QUIZ-01-05: accuracyByQuestionType 填充真实数据', async () => {
      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      const byType = snapshot.snapshot.quizMetrics.accuracyByQuestionType;

      // choice: 3 answers (2 correct, 1 wrong) → accuracy 0.67
      expect(byType.choice).toBeDefined();
      expect(byType.choice!.answeredCount).toBe(3);
      expect(byType.choice!.correctCount).toBe(2);
      expect(byType.choice!.accuracy).toBeCloseTo(0.67, 1);

      // fill: 2 answers (1 correct, 1 wrong) → accuracy 0.5
      expect(byType.fill).toBeDefined();
      expect(byType.fill!.answeredCount).toBe(2);
      expect(byType.fill!.correctCount).toBe(1);
      expect(byType.fill!.accuracy).toBe(0.5);

      // judge: 1 answer (1 correct)
      expect(byType.judge).toBeDefined();
      expect(byType.judge!.answeredCount).toBe(1);
      expect(byType.judge!.correctCount).toBe(1);
      expect(byType.judge!.accuracy).toBe(1.0);
    });

    it('M-QUIZ-01-05: 无该题型数据时不返回 key（标记缺失而非 0 分）', async () => {
      // Only choice data, no fill/judge
      prisma.quizAnswer.findMany.mockResolvedValue([
        { isCorrect: true, question: { type: 'choice' } },
      ]);

      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      const byType = snapshot.snapshot.quizMetrics.accuracyByQuestionType;

      expect(byType.choice).toBeDefined();
      expect(byType.fill).toBeUndefined();   // 无数据 → 不返回
      expect(byType.judge).toBeUndefined();  // 无数据 → 不返回
    });

    it('M-QUIZ-01-05: 跨用户数据隔离', async () => {
      await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      // quizAnswer.findMany 应以 attempt.userId 过滤
      const callArgs = prisma.quizAnswer.findMany.mock.calls[0][0];
      expect(callArgs.where.attempt.userId).toBe('u-001');
      expect(callArgs.where.attempt.finishedAt).toEqual({ not: null });
      expect(callArgs.where.attempt.startedAt).toBeDefined();
    });

    it('Snapshot 不含敏感字段', async () => {
      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      const serialized = JSON.stringify(snapshot);
      expect(serialized).not.toContain('Authorization');
      expect(serialized).not.toContain('JWT');
      expect(serialized).not.toContain('apiKey');
      expect(serialized).not.toContain('DATABASE_URL');
      expect(serialized).not.toContain('password');
    });

    it('窗口范围正确', async () => {
      const snapshot = await builder.build({ userId: 'u-001', triggerType: 'scheduled', operationId: 'sched-001' });
      expect(snapshot.snapshot.windowStart).toBeTruthy();
      expect(snapshot.snapshot.windowEnd).toBeTruthy();
      expect(new Date(snapshot.snapshot.windowEnd).getTime()).toBeGreaterThan(new Date(snapshot.snapshot.windowStart).getTime());
    });
  });

  describe('computeHash', () => {
    it('相同输入 → 相同 hash', async () => {
      const s1 = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      const s2 = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      expect(builder.computeHash(s1)).toBe(builder.computeHash(s2));
    });

    it('hash 长度 16，hex 格式', async () => {
      const s = await builder.build({ userId: 'u-001', triggerType: 'manual', operationId: 'op-001' });
      const hash = builder.computeHash(s);
      expect(hash).toHaveLength(16);
      expect(hash).toMatch(/^[0-9a-f]{16}$/);
    });
  });
});
