// src/services/db/gamification.db.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { Pool } from 'pg';
import { mockPoolInstance } from '../../tests/setup/tests-setup-unit';

// FIX 2: Un-mock the module we are testing.
vi.unmock('./gamification.db');

import { GamificationRepository } from './gamification.db';
import type { Achievement, UserAchievement, LeaderboardUser } from '../../types';

// Mock the logger. This is a server-side DB test.
vi.mock('../logger.server', () => ({
  logger: {
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    debug: vi.fn(),
  },
}));
import { logger as mockLogger } from '../logger.server';

describe('Gamification DB Service', () => {
  let gamificationRepo: GamificationRepository;

  beforeEach(() => {
    // Reset the global mock's call history before each test.
    vi.clearAllMocks();
    // Instantiate the repository with the mock pool for each test
    gamificationRepo = new GamificationRepository(mockPoolInstance as unknown as Pool);
  });

  describe('getAllAchievements', () => {
    it('should execute the correct SELECT query and return achievements', async () => {
      const mockAchievements: Achievement[] = [
        { achievement_id: 1, name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 },
      ];
      mockPoolInstance.query.mockResolvedValue({ rows: mockAchievements });

      const result = await gamificationRepo.getAllAchievements(mockLogger);

      expect(mockPoolInstance.query).toHaveBeenCalledWith('SELECT * FROM public.achievements ORDER BY points_value ASC, name ASC');
      expect(result).toEqual(mockAchievements);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(gamificationRepo.getAllAchievements(mockLogger)).rejects.toThrow('Failed to retrieve achievements.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError }, 'Database error in getAllAchievements');
    });
  });

  describe('getUserAchievements', () => {
    it('should execute the correct SELECT query with a JOIN and return user achievements', async () => {
      const mockUserAchievements: (UserAchievement & Achievement)[] = [
        { achievement_id: 1, user_id: 'user-123', achieved_at: '2024-01-01', name: 'First Steps', description: '...', icon: 'footprints', points_value: 10 },
      ];
      mockPoolInstance.query.mockResolvedValue({ rows: mockUserAchievements });

      const result = await gamificationRepo.getUserAchievements('user-123', mockLogger);

      expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('FROM public.user_achievements ua'), ['user-123']);
      expect(result).toEqual(mockUserAchievements);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(gamificationRepo.getUserAchievements('user-123', mockLogger)).rejects.toThrow('Failed to retrieve user achievements.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123' }, 'Database error in getUserAchievements');
    });
  });

  describe('awardAchievement', () => {
    it('should call the award_achievement database function with the correct parameters', async () => {
      mockPoolInstance.query.mockResolvedValue({ rows: [] }); // The function returns void
      await gamificationRepo.awardAchievement('user-123', 'Test Achievement', mockLogger);

      expect(mockPoolInstance.query).toHaveBeenCalledWith("SELECT public.award_achievement($1, $2)", ['user-123', 'Test Achievement']);
    });

    it('should throw ForeignKeyConstraintError if user or achievement does not exist', async () => {
      const dbError = new Error('violates foreign key constraint');
      (dbError as Error & { code: string }).code = '23503';
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(gamificationRepo.awardAchievement('non-existent-user', 'Non-existent Achievement', mockLogger)).rejects.toThrow('The specified user or achievement does not exist.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'non-existent-user', achievementName: 'Non-existent Achievement' }, 'Database error in awardAchievement');
    });

    it('should throw a generic error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(gamificationRepo.awardAchievement('user-123', 'Test Achievement', mockLogger)).rejects.toThrow('Failed to award achievement.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, userId: 'user-123', achievementName: 'Test Achievement' }, 'Database error in awardAchievement');
    });
  });

  describe('getLeaderboard', () => {
    it('should execute the correct SELECT query with a LIMIT and return leaderboard users', async () => {
      const mockLeaderboard: LeaderboardUser[] = [
        { user_id: 'user-1', full_name: 'User One', avatar_url: null, points: 500, rank: '1' },
        { user_id: 'user-2', full_name: 'User Two', avatar_url: null, points: 450, rank: '2' },
      ];
      mockPoolInstance.query.mockResolvedValue({ rows: mockLeaderboard });

      const result = await gamificationRepo.getLeaderboard(10, mockLogger);

      expect(mockPoolInstance.query).toHaveBeenCalledTimes(1);
      expect(mockPoolInstance.query).toHaveBeenCalledWith(expect.stringContaining('RANK() OVER (ORDER BY points DESC)'), [10]);
      expect(result).toEqual(mockLeaderboard);
    });

    it('should throw an error if the database query fails', async () => {
      const dbError = new Error('DB Error');
      mockPoolInstance.query.mockRejectedValue(dbError);
      await expect(gamificationRepo.getLeaderboard(10, mockLogger)).rejects.toThrow('Failed to retrieve leaderboard.');
      expect(mockLogger.error).toHaveBeenCalledWith({ err: dbError, limit: 10 }, 'Database error in getLeaderboard');
    });
  });
});