/**
 * Streak Calculator Unit Tests
 *
 * Tests for:
 * - Timezone-aware streak calculation
 * - Streak breaking after 2 days without activity
 * - Longest streak calculation
 * - Edge cases: DST, timezone boundaries, multiple exercises same day
 */

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { subDays, startOfDay } from 'date-fns';
import { toZonedTime } from 'date-fns-tz';

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

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

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

describe('StreakCalculator', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

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

  // ============================================
  // BASIC STREAK CALCULATION
  // ============================================

  describe('calculateStreak - basic functionality', () => {
    it('should return streak 0 when no activity', async () => {
      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([])  // recentActivity
        .mockResolvedValueOnce([]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      expect(result.currentStreak).toBe(0);
      expect(result.isStreakActive).toBe(false);
      expect(result.lastActivityDate).toBeNull();
    });

    it('should calculate streak of 1 for activity today', async () => {
      const today = new Date();

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: today }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      expect(result.currentStreak).toBe(1);
      expect(result.isStreakActive).toBe(true);
      expect(result.lastActivityDate).toBeInstanceOf(Date);
    });

    it('should calculate streak of 3 for 3 consecutive days', async () => {
      const today = new Date();
      const yesterday = subDays(today, 1);
      const twoDaysAgo = subDays(today, 2);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
          { createdAt: twoDaysAgo },
        ])  // recentActivity
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
          { createdAt: twoDaysAgo },
        ]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      expect(result.currentStreak).toBe(3);
      expect(result.isStreakActive).toBe(true);
    });
  });

  // ============================================
  // STREAK BREAKING
  // ============================================

  describe('calculateStreak - streak breaking', () => {
    it('should break streak if no activity for 2 days', async () => {
      const twoDaysAgo = subDays(new Date(), 2);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: twoDaysAgo }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: twoDaysAgo }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

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

    it('should keep streak active if activity yesterday', async () => {
      const yesterday = subDays(new Date(), 1);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: yesterday }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: yesterday }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      expect(result.currentStreak).toBe(1);
      expect(result.isStreakActive).toBe(true);
    });
  });

  // ============================================
  // TIMEZONE HANDLING
  // ============================================

  describe('calculateStreak - timezone handling', () => {
    it('should handle Argentina timezone (UTC-3)', async () => {
      // Simular actividad a las 23:00 hora local de Argentina (02:00 UTC del día siguiente)
      const now = new Date();
      const activityAt23PM = new Date(now);
      activityAt23PM.setHours(23, 0, 0, 0);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: activityAt23PM }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: activityAt23PM }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'America/Argentina/Buenos_Aires',
      });

      // Debe considerar como actividad "hoy" en timezone local
      expect(result.currentStreak).toBe(1);
      expect(result.isStreakActive).toBe(true);
    });

    it('should handle user traveling between timezones', async () => {
      const today = new Date();

      // Mock para calculateStreak (actividad reciente) y getLongestStreak - llamado dos veces
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity for NY
        .mockResolvedValueOnce([{ createdAt: today }])  // allActivity for NY
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity for Tokyo
        .mockResolvedValueOnce([{ createdAt: today }]); // allActivity for Tokyo

      // Usuario viajó de Nueva York a Tokyo
      const resultNY = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'America/New_York',
      });

      const resultTokyo = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'Asia/Tokyo',
      });

      // Ambos deben mostrar streak activo, posiblemente con diferente currentStreak
      // dependiendo de la hora local
      expect(resultNY.isStreakActive).toBe(true);
      expect(resultTokyo.isStreakActive).toBe(true);
    });

    it('should use UTC as default timezone', async () => {
      const today = new Date();

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: today }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        // Sin especificar timezone
      });

      expect(result.currentStreak).toBe(1);
      expect(result.isStreakActive).toBe(true);
    });
  });

  // ============================================
  // DAYLIGHT SAVING TIME (DST)
  // ============================================

  describe('calculateStreak - DST handling', () => {
    it('should handle DST transition in New York', async () => {
      // Simular actividad durante transición DST
      const today = new Date();
      const yesterday = subDays(today, 1);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
        ])  // recentActivity
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
        ]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'America/New_York',
      });

      // El cálculo debe ser consistente sin importar DST
      expect(result.currentStreak).toBeGreaterThanOrEqual(1);
      expect(result.isStreakActive).toBe(true);
    });

    it('should handle DST transition in Europe', async () => {
      const today = new Date();
      const yesterday = subDays(today, 1);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
        ])  // recentActivity
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
        ]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'Europe/Madrid',
      });

      expect(result.currentStreak).toBeGreaterThanOrEqual(1);
      expect(result.isStreakActive).toBe(true);
    });
  });

  // ============================================
  // MULTIPLE EXERCISES SAME DAY
  // ============================================

  describe('calculateStreak - multiple exercises same day', () => {
    it('should count only one day for multiple exercises', async () => {
      const today = new Date();
      const activity1 = new Date(today);
      activity1.setHours(9, 0, 0, 0);
      const activity2 = new Date(today);
      activity2.setHours(14, 0, 0, 0);
      const activity3 = new Date(today);
      activity3.setHours(20, 0, 0, 0);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([
          { createdAt: activity1 },
          { createdAt: activity2 },
          { createdAt: activity3 },
        ])  // recentActivity
        .mockResolvedValueOnce([
          { createdAt: activity1 },
          { createdAt: activity2 },
          { createdAt: activity3 },
        ]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      // Debe contar como 1 día, no 3
      expect(result.currentStreak).toBe(1);
    });
  });

  // ============================================
  // LONGEST STREAK
  // ============================================

  describe('getLongestStreak', () => {
    it('should return 0 when no activity', async () => {
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([]);

      const result = await StreakCalculator.getLongestStreak('user-1');

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

    it('should calculate longest streak correctly', async () => {
      const today = new Date();
      const yesterday = subDays(today, 1);
      const twoDaysAgo = subDays(today, 2);
      const fiveDaysAgo = subDays(today, 5);
      const sixDaysAgo = subDays(today, 6);

      // Streak actual: 3 días (hoy, ayer, anteayer)
      // Streak histórico: 2 días (hace 5 y 6 días)
      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([
        { createdAt: today },
        { createdAt: yesterday },
        { createdAt: twoDaysAgo },
        { createdAt: fiveDaysAgo },
        { createdAt: sixDaysAgo },
      ]);

      const result = await StreakCalculator.getLongestStreak('user-1');

      // El streak más largo es 3
      expect(result).toBe(3);
    });

    it('should handle single activity', async () => {
      const today = new Date();

      (prisma.exerciseAttempt.findMany as any).mockResolvedValueOnce([
        { createdAt: today },
      ]);

      const result = await StreakCalculator.getLongestStreak('user-1');

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

  // ============================================
  // HAS ACTIVITY ON DATE
  // ============================================

  describe('hasActivityOnDate', () => {
    it('should return true if activity exists on date', async () => {
      const date = new Date();

      (prisma.exerciseAttempt.count as any).mockResolvedValueOnce(1);

      const result = await StreakCalculator.hasActivityOnDate(
        'user-1',
        date,
        'UTC'
      );

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

    it('should return false if no activity on date', async () => {
      const date = new Date();

      (prisma.exerciseAttempt.count as any).mockResolvedValueOnce(0);

      const result = await StreakCalculator.hasActivityOnDate(
        'user-1',
        date,
        'UTC'
      );

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

  // ============================================
  // DAYS UNTIL BREAK
  // ============================================

  describe('daysUntilStreakBreaks', () => {
    it('should return 1 day if activity today', async () => {
      const today = new Date();

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: today }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      // Si actividad fue hoy, tiene hasta mañana (1 día completo)
      expect(result.daysUntilStreakBreaks).toBe(1);
    });

    it('should return fraction of day if activity yesterday', async () => {
      const yesterday = subDays(new Date(), 1);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: yesterday }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: yesterday }]); // allActivity for longestStreak

      const result = await StreakCalculator.calculateStreak({
        userId: 'user-1',
        timezone: 'UTC',
      });

      // Si actividad fue ayer, debe actuar hoy (fracción de día)
      expect(result.daysUntilStreakBreaks).toBeGreaterThan(0);
      expect(result.daysUntilStreakBreaks).toBeLessThan(1);
    });
  });

  // ============================================
  // GET USER STREAK INFO (Optimized version)
  // ============================================

  describe('getUserStreakInfo', () => {
    it('should return simplified streak info', async () => {
      const today = new Date();
      const yesterday = subDays(today, 1);
      const twoDaysAgo = subDays(today, 2);

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
          { createdAt: twoDaysAgo },
        ])  // recentActivity
        .mockResolvedValueOnce([
          { createdAt: today },
          { createdAt: yesterday },
          { createdAt: twoDaysAgo },
        ]); // allActivity for longestStreak

      const result = await StreakCalculator.getUserStreakInfo('user-1', 'UTC');

      expect(result.currentStreak).toBe(3);
      expect(result.hasStreakBonus).toBe(true);
    });

    it('should not have streak bonus for less than 3 days', async () => {
      const today = new Date();

      // Mock para calculateStreak (actividad reciente) y getLongestStreak
      (prisma.exerciseAttempt.findMany as any)
        .mockResolvedValueOnce([{ createdAt: today }])  // recentActivity
        .mockResolvedValueOnce([{ createdAt: today }]); // allActivity for longestStreak

      const result = await StreakCalculator.getUserStreakInfo('user-1', 'UTC');

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