/**
 * Score Calculator Unit Tests
 *
 * Tests for:
 * - Points calculation by difficulty
 * - Streak bonuses
 * - Hint penalties
 * - First attempt bonus
 * - Speed bonus
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ExerciseDifficulty } from '@prisma/client';

// Mock dependencies FIRST before importing
vi.mock('../../src/shared/database/prisma.client', () => ({
  prisma: {
    exercise: {
      findUnique: vi.fn(),
    },
    exerciseAttempt: {
      findMany: vi.fn(),
      aggregate: vi.fn(),
    },
  },
}));

vi.mock('../../src/shared/utils/logger', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));

// Mock StreakCalculator to avoid prisma calls
vi.mock('../../src/modules/ranking/calculators/streak.calculator', () => ({
  StreakCalculator: {
    calculateStreak: vi.fn().mockResolvedValue({
      currentStreak: 0,
      longestStreak: 0,
      lastActivityDate: null,
      isStreakActive: false,
      daysUntilStreakBreaks: 0,
    }),
    getLongestStreak: vi.fn().mockResolvedValue(0),
    getUserStreakInfo: vi.fn().mockResolvedValue({
      currentStreak: 0,
      hasStreakBonus: false,
    }),
    hasActivityOnDate: vi.fn().mockResolvedValue(false),
  },
}));

// Import mocked modules and service
import { prisma } from '../../src/shared/database/prisma.client';
import { ScoreCalculator } from '../../src/modules/ranking/calculators/score.calculator';
import { StreakCalculator } from '../../src/modules/ranking/calculators/streak.calculator';

describe('ScoreCalculator', () => {
  beforeEach(() => {
    vi.clearAllMocks();
    process.env.JWT_SECRET = 'test-jwt-secret';
  });

  afterEach(() => {
    vi.restoreAllMocks();
  });

  // ============================================
  // BASE POINTS TESTS
  // ============================================

  describe('calculate - base points', () => {
    it('should return 10 base points for BASIC difficulty', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.basePoints).toBe(10);
      expect(result.breakdown).toContain('Base: 10 puntos (BASIC)');
    });

    it('should return 20 base points for INTERMEDIATE difficulty', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.INTERMEDIATE,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.basePoints).toBe(20);
    });

    it('should return 30 base points for ADVANCED difficulty', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.ADVANCED,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.basePoints).toBe(30);
    });

    it('should return 40 base points for EXPERT difficulty', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.EXPERT,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.basePoints).toBe(40);
    });

    it('should throw error when exercise not found', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce(null);

      await expect(
        ScoreCalculator.calculate({
          exerciseId: 'nonexistent',
          userId: 'user-123',
          isCorrect: true,
          timeSpentSeconds: 30,
          hintsUsed: 0,
          attemptNumber: 1,
        })
      ).rejects.toThrow('Exercise nonexistent not found');
    });
  });

  // ============================================
  // INCORRECT ANSWER TESTS
  // ============================================

  describe('calculate - incorrect answers', () => {
    it('should return 0 points for incorrect answer', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.ADVANCED,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: false,
        timeSpentSeconds: 60,
        hintsUsed: 2,
        attemptNumber: 1,
      });

      expect(result.finalPoints).toBe(0);
      expect(result.breakdown).toContain('Incorrecto: 0 puntos');
    });
  });

  // ============================================
  // FIRST ATTEMPT BONUS TESTS
  // ============================================

  describe('calculate - first attempt bonus', () => {
    it('should add 20% bonus for first attempt (correct)', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.firstAttemptMultiplier).toBe(1.2);
      // Base 10 + 2 (20%) = 12
      expect(result.finalPoints).toBe(12);
      expect(result.breakdown).toContain('Primer intento: +2 puntos (+20%)');
    });

    it('should NOT add bonus for subsequent attempts', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 3, // Third attempt
      });

      expect(result.firstAttemptMultiplier).toBe(1.0);
      expect(result.finalPoints).toBe(10); // Only base points
    });
  });

  // ============================================
  // SPEED BONUS TESTS
  // ============================================

  describe('calculate - speed bonus', () => {
    it('should add 10% bonus for completing in under 60 seconds', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 45, // Under 60s threshold
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.speedMultiplier).toBe(1.1);
      // Base 10 + 2 (first attempt) + 1 (speed) = 13
      expect(result.finalPoints).toBe(13);
      expect(result.breakdown).toContain('Velocidad: +1 puntos (+10%, <60s)');
    });

    it('should NOT add speed bonus for over 60 seconds', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 90, // Over 60s
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.speedMultiplier).toBe(1.0);
    });
  });

  // ============================================
  // STREAK BONUS TESTS
  // ============================================

  describe('calculate - streak bonus', () => {
    it('should add 50% bonus for 3+ day streak', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });

      // Mock streak info with 3-day streak
      (StreakCalculator.getUserStreakInfo as any).mockResolvedValueOnce({
        currentStreak: 3,
        hasStreakBonus: true,
      });

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.streakMultiplier).toBe(1.5);
      // Base 10 + 2 (first) + 5 (streak) = 17
      expect(result.finalPoints).toBe(17);
      expect(result.breakdown.some(b => b.includes('Racha') && b.includes('+50%'))).toBe(true);
    });

    it('should NOT add streak bonus for less than 3 days', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });

      // Mock streak info with 1-day streak (no bonus)
      (StreakCalculator.getUserStreakInfo as any).mockResolvedValueOnce({
        currentStreak: 1,
        hasStreakBonus: false,
      });

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 0,
        attemptNumber: 1,
      });

      expect(result.streakMultiplier).toBe(1.0);
    });
  });

  // ============================================
  // HINT PENALTY TESTS
  // ============================================

  describe('calculate - hint penalty', () => {
    it('should deduct 2 points per hint used', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 3, // 3 hints = -6 points
        attemptNumber: 1,
      });

      expect(result.hintPenalty).toBe(6);
      // Base 10 + 2 (first) - 6 (hints) = 6
      expect(result.finalPoints).toBe(6);
      expect(result.breakdown).toContain('Pistas: -6 puntos (-2 cada una)');
    });

    it('should not return negative points', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.BASIC,
      });
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 120,
        hintsUsed: 10, // 10 hints = -20 points (more than base 10)
        attemptNumber: 3, // Not first attempt (no bonus)
      });

      expect(result.finalPoints).toBe(0); // Minimum 0
    });
  });

  // ============================================
  // COMBINED BONUSES TESTS
  // ============================================

  describe('calculate - combined bonuses', () => {
    it('should calculate all bonuses combined correctly', async () => {
      (prisma.exercise.findUnique as any).mockResolvedValueOnce({
        difficulty: ExerciseDifficulty.INTERMEDIATE, // Base 20
      });

      // Mock 3-day streak
      (StreakCalculator.getUserStreakInfo as any).mockResolvedValueOnce({
        currentStreak: 3,
        hasStreakBonus: true,
      });

      const result = await ScoreCalculator.calculate({
        exerciseId: 'exercise-123',
        userId: 'user-123',
        isCorrect: true,
        timeSpentSeconds: 45, // Speed bonus
        hintsUsed: 2, // -4 points
        attemptNumber: 1, // First attempt bonus
      });

      // Base: 20
      // First attempt: +4 (20% of 20)
      // Speed: +2 (10% of 20)
      // Streak: +10 (50% of 20)
      // Hint penalty: -4
      // Total: 20 + 4 + 2 + 10 - 4 = 32

      expect(result.basePoints).toBe(20);
      expect(result.firstAttemptMultiplier).toBe(1.2);
      expect(result.speedMultiplier).toBe(1.1);
      expect(result.streakMultiplier).toBe(1.5);
      expect(result.hintPenalty).toBe(4);
      expect(result.finalPoints).toBe(32);
    });
  });

  // ============================================
  // GET USER STREAK TESTS
  // ============================================

  describe('getUserStreak', () => {
    it('should return streak 0 when no attempts', async () => {
      (StreakCalculator.getUserStreakInfo as any).mockResolvedValueOnce({
        currentStreak: 0,
        hasStreakBonus: false,
      });

      const result = await ScoreCalculator.getUserStreak('user-123');

      expect(result.currentStreak).toBe(0);
      expect(result.hasStreakBonus).toBe(false);
    });

    it('should break streak if last attempt is older than yesterday', async () => {
      (StreakCalculator.getUserStreakInfo as any).mockResolvedValueOnce({
        currentStreak: 0,
        hasStreakBonus: false,
      });

      const result = await ScoreCalculator.getUserStreak('user-123');

      expect(result.currentStreak).toBe(0);
      expect(result.hasStreakBonus).toBe(false);
    });
  });

  // ============================================
  // QUICK SCORE TESTS
  // ============================================

  describe('getQuickScore', () => {
    it('should return quick score without full calculation', () => {
      const result = ScoreCalculator.getQuickScore(
        ExerciseDifficulty.ADVANCED,
        true,
        2 // 2 hints
      );

      // Base 30 - 4 (2 hints * 2) = 26
      expect(result).toBe(26);
    });

    it('should return 0 for incorrect answer', () => {
      const result = ScoreCalculator.getQuickScore(
        ExerciseDifficulty.ADVANCED,
        false,
        0
      );

      expect(result).toBe(0);
    });

    it('should not return negative score', () => {
      const result = ScoreCalculator.getQuickScore(
        ExerciseDifficulty.BASIC,
        true,
        10 // More hints than base points
      );

      expect(result).toBe(0);
    });
  });

  // ============================================
  // MAX POSSIBLE POINTS TESTS
  // ============================================

  describe('getMaxPossiblePoints', () => {
    it('should calculate maximum possible points with streak', () => {
      const result = ScoreCalculator.getMaxPossiblePoints(
        ExerciseDifficulty.INTERMEDIATE,
        true // Has streak
      );

      // Base 20
      // First attempt (20%): 24
      // Speed (10%): 27
      // Streak (50%): 41
      expect(result).toBeGreaterThan(30);
    });

    it('should calculate maximum points without streak', () => {
      const result = ScoreCalculator.getMaxPossiblePoints(
        ExerciseDifficulty.BASIC,
        false
      );

      // Base 10, with first attempt and speed only
      expect(result).toBeGreaterThan(10);
    });
  });

  // ============================================
  // CALCULATE USER TOTAL POINTS TESTS
  // ============================================

  describe('calculateUserTotalPoints', () => {
    it('should aggregate all correct attempt points', async () => {
      (prisma.exerciseAttempt.aggregate as any).mockResolvedValueOnce({
        _sum: { pointsEarned: 150 },
      });

      const result = await ScoreCalculator.calculateUserTotalPoints('user-123');

      expect(result).toBe(150);
    });

    it('should return 0 when no points earned', async () => {
      (prisma.exerciseAttempt.aggregate as any).mockResolvedValueOnce({
        _sum: { pointsEarned: null },
      });

      const result = await ScoreCalculator.calculateUserTotalPoints('user-123');

      expect(result).toBe(0);
    });
  });

  // ============================================
  // CALCULATE USER MODULE POINTS TESTS
  // ============================================

  describe('calculateUserModulePoints', () => {
    it('should aggregate points for specific module', async () => {
      (prisma.exerciseAttempt.aggregate as any).mockResolvedValueOnce({
        _sum: { pointsEarned: 50 },
      });

      const result = await ScoreCalculator.calculateUserModulePoints(
        'user-123',
        'module-123'
      );

      expect(result).toBe(50);
      expect(prisma.exerciseAttempt.aggregate).toHaveBeenCalledWith({
        where: {
          userId: 'user-123',
          status: 'CORRECT',
          exercise: { moduleId: 'module-123' },
        },
        _sum: { pointsEarned: true },
      });
    });
  });
});