// src/services/gamificationService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { gamificationService } from './gamificationService';
import { gamificationRepo } from './db/index.db';
import { ForeignKeyConstraintError } from './db/errors.db';
import { logger as mockLogger } from './logger.server';
import {
  createMockAchievement,
  createMockLeaderboardUser,
  createMockUserAchievement,
} from '../tests/utils/mockFactories';

// Mock dependencies
vi.mock('./db/index.db', () => ({
  gamificationRepo: {
    awardAchievement: vi.fn(),
    getAllAchievements: vi.fn(),
    getLeaderboard: vi.fn(),
    getUserAchievements: vi.fn(),
  },
}));

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

// Mock the error class
vi.mock('./db/errors.db', () => ({
  ForeignKeyConstraintError: class extends Error {
    constructor(message: string) {
      super(message);
      this.name = 'ForeignKeyConstraintError';
    }
  },
}));

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

  describe('awardAchievement', () => {
    it('should call the repository to award an achievement', async () => {
      const userId = 'user-123';
      const achievementName = 'First-Upload';
      vi.mocked(gamificationRepo.awardAchievement).mockResolvedValue(undefined);

      await gamificationService.awardAchievement(userId, achievementName, mockLogger);

      expect(gamificationRepo.awardAchievement).toHaveBeenCalledWith(userId, achievementName, mockLogger);
    });

    it('should re-throw ForeignKeyConstraintError without logging it as a service error', async () => {
      const userId = 'user-123';
      const achievementName = 'NonExistentAchievement';
      const fkError = new ForeignKeyConstraintError('Achievement not found');
      vi.mocked(gamificationRepo.awardAchievement).mockRejectedValue(fkError);

      await expect(
        gamificationService.awardAchievement(userId, achievementName, mockLogger),
      ).rejects.toThrow(fkError);

      expect(mockLogger.error).not.toHaveBeenCalled();
    });

    it('should log and re-throw generic errors', async () => {
      const userId = 'user-123';
      const achievementName = 'First-Upload';
      const dbError = new Error('DB connection failed');
      vi.mocked(gamificationRepo.awardAchievement).mockRejectedValue(dbError);

      await expect(
        gamificationService.awardAchievement(userId, achievementName, mockLogger),
      ).rejects.toThrow(dbError);

      expect(mockLogger.error).toHaveBeenCalledWith(
        { error: dbError, userId, achievementName },
        'Error awarding achievement via admin endpoint:',
      );
    });
  });

  describe('getAllAchievements', () => {
    it('should return all achievements from the repository', async () => {
      const mockAchievements = [
        createMockAchievement({ name: 'Achieve1' }),
        createMockAchievement({ name: 'Achieve2' }),
      ];
      vi.mocked(gamificationRepo.getAllAchievements).mockResolvedValue(mockAchievements);

      const result = await gamificationService.getAllAchievements(mockLogger);

      expect(result).toEqual(mockAchievements);
      expect(gamificationRepo.getAllAchievements).toHaveBeenCalledWith(mockLogger);
    });

    it('should log and re-throw an error if the repository fails', async () => {
      const dbError = new Error('DB Error');
      vi.mocked(gamificationRepo.getAllAchievements).mockRejectedValue(dbError);

      await expect(gamificationService.getAllAchievements(mockLogger)).rejects.toThrow(dbError);

      expect(mockLogger.error).toHaveBeenCalledWith(
        { error: dbError },
        'Error in getAllAchievements service method',
      );
    });
  });

  describe('getLeaderboard', () => {
    it('should return the leaderboard from the repository', async () => {
      const mockLeaderboard = [createMockLeaderboardUser({ rank: '1' })];
      vi.mocked(gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);

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

      expect(result).toEqual(mockLeaderboard);
      expect(gamificationRepo.getLeaderboard).toHaveBeenCalledWith(10, mockLogger);
    });

    it('should log and re-throw an error if the repository fails', async () => {
      const dbError = new Error('DB Error');
      vi.mocked(gamificationRepo.getLeaderboard).mockRejectedValue(dbError);

      await expect(gamificationService.getLeaderboard(10, mockLogger)).rejects.toThrow(dbError);

      expect(mockLogger.error).toHaveBeenCalledWith(
        { error: dbError, limit: 10 },
        'Error fetching leaderboard in service method.',
      );
    });
  });

  describe('getUserAchievements', () => {
    it("should return a user's achievements from the repository", async () => {
      const userId = 'user-123';
      const mockUserAchievements = [createMockUserAchievement({ user_id: userId })];
      vi.mocked(gamificationRepo.getUserAchievements).mockResolvedValue(mockUserAchievements);

      const result = await gamificationService.getUserAchievements(userId, mockLogger);

      expect(result).toEqual(mockUserAchievements);
      expect(gamificationRepo.getUserAchievements).toHaveBeenCalledWith(userId, mockLogger);
    });

    it('should log and re-throw an error if the repository fails', async () => {
      const userId = 'user-123';
      const dbError = new Error('DB Error');
      vi.mocked(gamificationRepo.getUserAchievements).mockRejectedValue(dbError);

      await expect(gamificationService.getUserAchievements(userId, mockLogger)).rejects.toThrow(
        dbError,
      );

      expect(mockLogger.error).toHaveBeenCalledWith(
        { error: dbError, userId },
        'Error fetching user achievements in service method.',
      );
    });
  });
});