import { SnapshotBuilderService } from './snapshot-builder.service';

describe('SnapshotBuilderService', () => {
  let service: SnapshotBuilderService;
  let mockPrisma: any;
  let mockPriorityRules: any;

  const u1 = 'u1';
  const m1 = 'm1';

  // Lazy access — private methods only available after beforeEach
  const svc = () => service as any;

  beforeEach(() => {
    mockPriorityRules = {
      computePriorityRules: jest.fn().mockReturnValue({ depthPreference: 'standard' }),
    };

    mockPrisma = {
      userAiSettings: { findUnique: jest.fn().mockResolvedValue(null) },
      userLearningProfile: { findUnique: jest.fn().mockResolvedValue(null) },
      learningSession: {
        aggregate: jest.fn().mockResolvedValue({ _sum: { totalActiveSeconds: 0 }, _count: 0 }),
        count: jest.fn().mockResolvedValue(0),
        findMany: jest.fn().mockResolvedValue([]),
      },
      dailyLearningActivity: {
        findMany: jest.fn().mockResolvedValue([]),
        findUnique: jest.fn().mockResolvedValue(null),
      },
      learningRecord: { findMany: jest.fn().mockResolvedValue([]) },
      materialReadingProgress: {
        findMany: jest.fn().mockResolvedValue([]),
        findFirst: jest.fn().mockResolvedValue(null),
        aggregate: jest.fn().mockResolvedValue({ _sum: { totalActiveSeconds: 0, sessionCount: 0 }, _count: 0 }),
      },
      knowledgeItem: { findMany: jest.fn().mockResolvedValue([]) },
      quizAttempt: {
        aggregate: jest.fn().mockResolvedValue({ _avg: { score: null, correctCount: null, totalQuestions: null }, _count: 0 }),
        findMany: jest.fn().mockResolvedValue([]),
      },
      reviewLog: { findMany: jest.fn().mockResolvedValue([]) },
      aiLearningAnalysis: { findMany: jest.fn().mockResolvedValue([]) },
      weakPointCandidate: { findMany: jest.fn().mockResolvedValue([]) },
      streakRecord: { findMany: jest.fn().mockResolvedValue([]) },
      readingEvent: {
        findFirst: jest.fn().mockResolvedValue(null),
        findMany: jest.fn().mockResolvedValue([]),
      },
      userDevice: { findMany: jest.fn().mockResolvedValue([]) },
      learningAnalysisSnapshot: { create: jest.fn().mockImplementation((args: any) => Promise.resolve({ id: 'snap-1', ...args.data })) },
    };

    service = new SnapshotBuilderService(mockPrisma, mockPriorityRules);
  });

  // ═══════════════════════════════════════════════════════════════════
  // buildSnapshot — basic flow
  // ═══════════════════════════════════════════════════════════════════

  describe('buildSnapshot', () => {
    it('builds with defaults (all data empty)', async () => {
      const snap = await service.buildSnapshot(u1, 'material', m1);
      expect(snap).toBeDefined();
      expect(snap.userId).toBe(u1);
      expect(snap.snapshotVersion).toBe('ai_snapshot_v1');
      expect(snap.sourceDataVersion).toBe('1.0');
      expect(snap.expiresAt).toBeInstanceOf(Date);
      expect(snap.privacyScope).toEqual({ allowDocumentContent: false, allowLearningBehavior: true, allowUserProfile: true });
      expect((snap.constraints as any).qualityPreference).toBe('standard');
      expect(snap.allowedModelFields).toContain('scoreSignals');
    });

    it('respects all privacy flags disabled', async () => {
      mockPrisma.userAiSettings.findUnique.mockResolvedValue({
        allowUseDocumentContent: false, allowUseLearningBehavior: false, allowUseUserProfile: false,
      });
      const snap = await service.buildSnapshot(u1, 'material', m1);
      expect((snap.privacyScope as any).allowLearningBehavior).toBe(false);
      expect(snap.userProfile).toBeUndefined();
      expect(snap.learningBehaviorSummary).toBeUndefined();
      expect(snap.behaviorSignals).toBeUndefined();
      expect(snap.contentStructureSummary).toBeUndefined();
    });

    it('calls computePriorityRules with profile + settings', async () => {
      mockPrisma.userAiSettings.findUnique.mockResolvedValue({ allowAiAnalysis: true });
      mockPrisma.userLearningProfile.findUnique.mockResolvedValue({ learningGoal: 'exam' });
      await service.buildSnapshot(u1, 'knowledge_base', 'kb1');
      expect(mockPriorityRules.computePriorityRules).toHaveBeenCalledWith(
        expect.objectContaining({ learningGoal: 'exam' }),
        expect.objectContaining({ allowAiAnalysis: true }),
        'knowledge_base', 'kb1',
      );
    });

    it('creates snapshot record in DB', async () => {
      await service.buildSnapshot(u1, 'material', m1);
      expect(mockPrisma.learningAnalysisSnapshot.create).toHaveBeenCalledWith(
        expect.objectContaining({ data: expect.objectContaining({ userId: u1 }) }),
      );
    });

    it('logs error and re-throws', async () => {
      const loggerSpy = jest.spyOn(svc().logger, 'error');
      mockPrisma.userAiSettings.findUnique.mockRejectedValueOnce(new Error('DB down'));
      await expect(service.buildSnapshot(u1, 'material', m1)).rejects.toThrow('DB down');
      expect(loggerSpy).toHaveBeenCalled();
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // Scope routing
  // ═══════════════════════════════════════════════════════════════════

  describe('scope routing', () => {
    it('scopes by materialId for material type', async () => {
      await service.buildSnapshot(u1, 'material', m1);
      expect(mockPrisma.learningSession.aggregate.mock.calls[0][0].where.materialId).toBe(m1);
    });
    it('scopes by knowledgeBaseId for knowledge_base', async () => {
      await service.buildSnapshot(u1, 'knowledge_base', 'kb1');
      expect(mockPrisma.learningSession.aggregate.mock.calls[0][0].where.knowledgeBaseId).toBe('kb1');
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeWeightedQuizScore
  // ═══════════════════════════════════════════════════════════════════

  describe('computeWeightedQuizScore', () => {
    it('returns null for empty', () => {
      expect(svc().computeWeightedQuizScore([], new Date())).toBeNull();
    });
    it('returns weighted score', () => {
      expect(svc().computeWeightedQuizScore([{ score: 80, startedAt: new Date() }], new Date())).toBe(0.8);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeWeightedReviewScore
  // ═══════════════════════════════════════════════════════════════════

  describe('computeWeightedReviewScore', () => {
    it('returns null for empty', () => {
      expect(svc().computeWeightedReviewScore([], new Date())).toBeNull();
    });
    it('maps easy rating to 1.0', () => {
      expect(svc().computeWeightedReviewScore([{ rating: 'easy', reviewedAt: new Date() }], new Date())).toBe(1.0);
    });
    it('defaults unknown to 0.5', () => {
      expect(svc().computeWeightedReviewScore([{ rating: 'unknown', reviewedAt: new Date() }], new Date())).toBe(0.5);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeAIScore
  // ═══════════════════════════════════════════════════════════════════

  describe('computeAIScore', () => {
    it('returns null for empty', () => { expect(svc().computeAIScore([])).toBeNull(); });
    it('returns null when no valid state', () => {
      expect(svc().computeAIScore([{ learningState: null, confidence: 0.5 }])).toBeNull();
    });
    it('returns weighted score', () => {
      expect(svc().computeAIScore([{ learningState: 'mastered', confidence: 0.9 }])).toBe(0.9);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeWeakPointSeverity
  // ═══════════════════════════════════════════════════════════════════

  describe('computeWeakPointSeverity', () => {
    it('returns null for empty', () => { expect(svc().computeWeakPointSeverity([])).toBeNull(); });
    it('caps at 1.0', () => {
      expect(svc().computeWeakPointSeverity(Array(10).fill({ confidence: 1 }))).toBeLessThanOrEqual(1);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeQuizTrend
  // ═══════════════════════════════════════════════════════════════════

  describe('computeQuizTrend', () => {
    const q = (score: number, daysAgo: number) => ({ score, startedAt: new Date(Date.now() - daysAgo * 86400000) });

    it('< 4 → insufficient_data', () => {
      expect(svc().computeQuizTrend([q(80, 1)]).direction).toBe('insufficient_data');
    });
    it('detects increasing', () => {
      expect(svc().computeQuizTrend([q(50, 10), q(50, 8), q(80, 3), q(90, 1)]).direction).toBe('increasing');
    });
    it('detects decreasing', () => {
      expect(svc().computeQuizTrend([q(90, 10), q(90, 8), q(50, 3), q(50, 1)]).direction).toBe('decreasing');
    });
    it('detects stable', () => {
      expect(svc().computeQuizTrend([q(70, 10), q(70, 8), q(72, 3), q(71, 1)]).direction).toBe('stable');
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeCompositeMastery
  // ═══════════════════════════════════════════════════════════════════

  describe('computeCompositeMastery', () => {
    it('returns null when all null', () => {
      const w = svc().getScoreWeights('standard');
      expect(svc().computeCompositeMastery(null, null, null, null, w)).toBeNull();
    });
    it('computes from quiz only', () => {
      const w = svc().getScoreWeights('standard');
      expect(svc().computeCompositeMastery(0.8, null, null, 0, w)).toBe(0.8);
    });
    it('penalizes weak points', () => {
      const w = svc().getScoreWeights('standard');
      const base = svc().computeCompositeMastery(0.8, null, null, 0, w);
      const penalized = svc().computeCompositeMastery(0.8, null, null, 0.5, w);
      expect(penalized).toBeLessThan(base);
    });
    it('clamps to [0,1]', () => {
      const w = svc().getScoreWeights('standard');
      expect(svc().computeCompositeMastery(1.5, null, null, 0, w)).toBe(1);
      expect(svc().computeCompositeMastery(-0.5, null, null, 0, w)).toBe(0);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // classifyMasteryLevel
  // ═══════════════════════════════════════════════════════════════════

  describe('classifyMasteryLevel', () => {
    it('null→unknown', () => { expect(svc().classifyMasteryLevel(null)).toBe('unknown'); });
    it('0.1→struggling', () => { expect(svc().classifyMasteryLevel(0.1)).toBe('struggling'); });
    it('0.25→developing', () => { expect(svc().classifyMasteryLevel(0.25)).toBe('developing'); });
    it('0.5→progressing', () => { expect(svc().classifyMasteryLevel(0.5)).toBe('progressing'); });
    it('0.7→proficient', () => { expect(svc().classifyMasteryLevel(0.7)).toBe('proficient'); });
    it('0.85→mastered', () => { expect(svc().classifyMasteryLevel(0.85)).toBe('mastered'); });
  });

  // ═══════════════════════════════════════════════════════════════════
  // normalizeQualityPreference / getScoreWeights
  // ═══════════════════════════════════════════════════════════════════

  describe('normalizeQualityPreference', () => {
    it.each(['light', 'standard', 'deep', 'exam'] as const)('%s passes through', (v) => {
      expect(svc().normalizeQualityPreference(v)).toBe(v);
    });
    it('falls back to standard', () => {
      expect(svc().normalizeQualityPreference('fast')).toBe('standard');
      expect(svc().normalizeQualityPreference(null)).toBe('standard');
    });
  });

  describe('getScoreWeights', () => {
    it('exam > light for quiz weight', () => {
      expect(svc().getScoreWeights('exam').w_quiz).toBeGreaterThan(svc().getScoreWeights('light').w_quiz);
    });
    it('deep > exam for AI weight', () => {
      expect(svc().getScoreWeights('deep').w_ai).toBeGreaterThan(svc().getScoreWeights('exam').w_ai);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // mapPlatformCategory
  // ═══════════════════════════════════════════════════════════════════

  describe('mapPlatformCategory', () => {
    it('ios/android → phone', () => {
      expect(svc().mapPlatformCategory('ios')).toBe('phone');
      expect(svc().mapPlatformCategory('android')).toBe('phone');
    });
    it('ipados → tablet', () => { expect(svc().mapPlatformCategory('ipados')).toBe('tablet'); });
    it('desktop OS', () => {
      expect(svc().mapPlatformCategory('macos')).toBe('desktop');
      expect(svc().mapPlatformCategory('windows')).toBe('desktop');
    });
    it('web', () => { expect(svc().mapPlatformCategory('web')).toBe('web'); });
    it('null → null', () => { expect(svc().mapPlatformCategory(null)).toBeNull(); });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeSwitchFrequency
  // ═══════════════════════════════════════════════════════════════════

  describe('computeSwitchFrequency', () => {
    const e = (p: string) => ({ platform: p });
    it('< 2 → unknown', () => { expect(svc().computeSwitchFrequency([e('ios')])).toBe('unknown'); });
    it('none → rarely', () => { expect(svc().computeSwitchFrequency([e('ios'), e('ios')])).toBe('rarely'); });
    it('frequent → daily', () => {
      expect(svc().computeSwitchFrequency([e('ios'), e('macos'), e('ios'), e('macos')])).toBe('daily');
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // mapDeviceTaskSuitability
  // ═══════════════════════════════════════════════════════════════════

  describe('mapDeviceTaskSuitability', () => {
    it('phone restricts deep_analysis', () => {
      const r = svc().mapDeviceTaskSuitability('phone');
      expect(r.unsuitableTaskTypes).toContain('deep_analysis');
    });
    it('desktop allows everything', () => {
      expect(svc().mapDeviceTaskSuitability('desktop').unsuitableTaskTypes).toEqual([]);
    });
    it('null → empty', () => {
      expect(svc().mapDeviceTaskSuitability(null).suitableTaskTypes).toEqual([]);
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // modeOf
  // ═══════════════════════════════════════════════════════════════════

  describe('modeOf', () => {
    it('empty → null', () => { expect(svc().modeOf([])).toBeNull(); });
    it('returns most frequent', () => { expect(svc().modeOf([3, 1, 3, 2, 3])).toBe(3); });
    it('first on tie', () => { expect(svc().modeOf([1, 2])).toBe(1); });
  });

  // ═══════════════════════════════════════════════════════════════════
  // buildAllowedFields
  // ═══════════════════════════════════════════════════════════════════

  describe('buildAllowedFields', () => {
    it('core fields always', () => {
      expect(svc().buildAllowedFields(null, null)).toContain('constraints');
    });
    it('userProfile omitted when flag=false', () => {
      expect(svc().buildAllowedFields({ allowUseUserProfile: false }, null)).not.toContain('userProfile');
    });
    it('contentStructureSummary when flag=true', () => {
      expect(svc().buildAllowedFields({ allowUseDocumentContent: true }, null)).toContain('contentStructureSummary');
    });
  });

  // ═══════════════════════════════════════════════════════════════════
  // countBy
  // ═══════════════════════════════════════════════════════════════════

  describe('countBy', () => {
    it('counts', () => {
      expect(svc().countBy([{ t: 'a' }, { t: 'b' }, { t: 'a' }], (i: any) => i.t)).toEqual({ a: 2, b: 1 });
    });
    it('empty → {}', () => { expect(svc().countBy([], (i: any) => i)).toEqual({}); });
  });

  // ═══════════════════════════════════════════════════════════════════
  // computeSignals
  // ═══════════════════════════════════════════════════════════════════

  describe('computeSignals', () => {
    const emptyAgg = { _sum: { totalActiveSeconds: 0, sessionCount: 0 }, _count: 0 };

    it('returns all signal groups for empty data', () => {
      const r = svc().computeSignals([], 0, 0, null, new Date(), [], [], [], emptyAgg);
      expect(r.totalWeeklySessions).toBe(0);
      expect(r.engagementSignal).toBe('low');
      expect(r.consistency.label).toBe('irregular');
      expect(r.streak.currentStreak).toBe(0);
      expect(r.fatigue.fatigueRisk).toBe(false);
    });

    it('high engagement for active user', () => {
      const now = new Date();
      const activities = Array.from({ length: 7 }, (_, i) => ({
        activityDate: new Date(now.getTime() - i * 86400000),
        durationSeconds: 3600, sessionsCount: 1, readingSeconds: 1800, materialsReadCount: 1,
        activeRecallCount: 0, reviewCount: 0, aiAnalysisCount: 0, activityLevel: 3,
      }));
      const r = svc().computeSignals(activities, 7, 7, { durationSeconds: 1800, sessionsCount: 2, activityLevel: 3 }, now, [], [], [], {
        _sum: { totalActiveSeconds: 10000, sessionCount: 20 }, _count: 5,
      });
      expect(r.engagementSignal).toBe('high');
    });
  });
});
