// src/routes/gamification.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import { Request, Response, NextFunction } from 'express';
import gamificationRouter from './gamification.routes';
import * as db from '../services/db/index.db';
import { createMockUserProfile, createMockAchievement, createMockUserAchievement, createMockLeaderboardUser } from '../tests/utils/mockFactories';
import { ForeignKeyConstraintError } from '../services/db/errors.db';
import { createTestApp } from '../tests/utils/createTestApp';

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

// Mock the logger to keep test output clean
vi.mock('../services/logger.server', () => ({
  logger: {
    info: vi.fn(),
    debug: vi.fn(),
    error: vi.fn(),
    warn: vi.fn(),
  },
}));

// Use vi.hoisted to create mutable mock function references.
const mockedAuthMiddleware = vi.hoisted(() => vi.fn((req: Request, res: Response, next: NextFunction) => next()));
const mockedIsAdmin = vi.hoisted(() => vi.fn());

vi.mock('./passport.routes', () => ({
  default: {
    // The authenticate method will now call our hoisted mock middleware.
    authenticate: vi.fn(() => mockedAuthMiddleware),
  },
  // Mock the named export 'isAdmin'
  isAdmin: mockedIsAdmin,
}));

// Define a reusable matcher for the logger object.
const expectLogger = expect.objectContaining({
  info: expect.any(Function),
  error: expect.any(Function),
});

describe('Gamification Routes (/api/achievements)', () => {
  const mockUserProfile = createMockUserProfile({ user_id: 'user-123', points: 100 });
  const mockAdminProfile = createMockUserProfile({ user_id: 'admin-456', role: 'admin', points: 999 });

  beforeEach(() => {
    vi.clearAllMocks();
    // Default mock for authentication to simulate an unauthenticated user.
    mockedAuthMiddleware.mockImplementation((req: Request, res: Response) => {
      res.status(401).json({ message: 'Unauthorized' });
    });
    mockedIsAdmin.mockImplementation((req: Request, res: Response) => {
      res.status(403).json({ message: 'Forbidden' });
    });
  });

  const basePath = '/api/achievements';
  const unauthenticatedApp = createTestApp({ router: gamificationRouter, basePath });
  const authenticatedApp = createTestApp({ router: gamificationRouter, basePath, authenticatedUser: mockUserProfile });
  const adminApp = createTestApp({ router: gamificationRouter, basePath, authenticatedUser: mockAdminProfile });
  const errorHandler = (err: any, req: any, res: any, next: any) => {
    res.status(err.status || 500).json({ message: err.message, errors: err.errors });
  };
  unauthenticatedApp.use(errorHandler);
  authenticatedApp.use(errorHandler);
  adminApp.use(errorHandler);

  describe('GET /', () => {
    it('should return a list of all achievements (public endpoint)', async () => {
      const mockAchievements = [createMockAchievement({ achievement_id: 1 }), createMockAchievement({ achievement_id: 2 })];
      vi.mocked(db.gamificationRepo.getAllAchievements).mockResolvedValue(mockAchievements);

      const response = await supertest(unauthenticatedApp).get('/api/achievements');
      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockAchievements);
      expect(db.gamificationRepo.getAllAchievements).toHaveBeenCalledWith(expectLogger);
    });

    it('should return 500 if the database call fails', async () => {
      const dbError = new Error('DB Connection Failed');
      vi.mocked(db.gamificationRepo.getAllAchievements).mockRejectedValue(dbError);

      const response = await supertest(unauthenticatedApp).get('/api/achievements');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Connection Failed');
    });

    it('should return 400 if awarding an achievement to a non-existent user', async () => {
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockAdminProfile;
        next();
      });
      mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
      vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new ForeignKeyConstraintError('User not found'));

      const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
      expect(response.status).toBe(400);
      expect(response.body.message).toBe('User not found');
    });
  });

  describe('GET /me', () => {
    it('should return 401 Unauthorized when user is not authenticated', async () => {
      const response = await supertest(unauthenticatedApp).get('/api/achievements/me');
      expect(response.status).toBe(401);
    });

    it('should return achievements for the authenticated user', async () => {
      // Mock authentication to simulate a logged-in user.
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockUserProfile;
        next();
      });

      const mockUserAchievements = [createMockUserAchievement({ achievement_id: 1, user_id: 'user-123' })];
      vi.mocked(db.gamificationRepo.getUserAchievements).mockResolvedValue(mockUserAchievements);

      const response = await supertest(authenticatedApp).get('/api/achievements/me');

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockUserAchievements);
      expect(db.gamificationRepo.getUserAchievements).toHaveBeenCalledWith('user-123', expectLogger);
    });

    it('should return 500 if the database call fails', async () => {
      // Mock an authenticated user
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockUserProfile;
        next();
      });
      const dbError = new Error('DB Error');
      vi.mocked(db.gamificationRepo.getUserAchievements).mockRejectedValue(dbError);
      const response = await supertest(authenticatedApp).get('/api/achievements/me');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });
  });

  describe('POST /award', () => {
    const awardPayload = { userId: 'user-789', achievementName: 'Test Award' };

    it('should return 401 Unauthorized if user is not authenticated', async () => {
      const response = await supertest(unauthenticatedApp).post('/api/achievements/award').send(awardPayload);
      expect(response.status).toBe(401);
    });

    it('should return 403 Forbidden if authenticated user is not an admin', async () => {
      // Mock a regular authenticated user
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockUserProfile;
        next();
      });
      // Let the default isAdmin mock (set in beforeEach) run, which denies access

      const response = await supertest(authenticatedApp).post('/api/achievements/award').send(awardPayload);
      expect(response.status).toBe(403);
    });

    it('should successfully award an achievement when user is an admin', async () => {
      // Mock an authenticated admin user
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockAdminProfile;
        next();
      });
      mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next()); // Grant admin access
      vi.mocked(db.gamificationRepo.awardAchievement).mockResolvedValue(undefined);

      const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);

      expect(response.status).toBe(200);
      expect(response.body.message).toContain('Successfully awarded');
      expect(db.gamificationRepo.awardAchievement).toHaveBeenCalledTimes(1);
      expect(db.gamificationRepo.awardAchievement).toHaveBeenCalledWith(awardPayload.userId, awardPayload.achievementName, expectLogger);
    });

    it('should return 500 if the database call fails', async () => {
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockAdminProfile;
        next();
      });
      mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
      vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new Error('DB Error'));

      const response = await supertest(adminApp).post('/api/achievements/award').send(awardPayload);
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });

    it('should return 400 for an invalid userId or achievementName', async () => {
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => { req.user = mockAdminProfile; next(); });
      mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());

      const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: '', achievementName: '' });
      expect(response.status).toBe(400);
      expect(response.body.errors).toHaveLength(2);
    });

    it('should return 400 if awarding an achievement to a non-existent user', async () => {
      mockedAuthMiddleware.mockImplementation((req: Request, res: Response, next: NextFunction) => {
        req.user = mockAdminProfile;
        next();
      });
      mockedIsAdmin.mockImplementation((req: Request, res: Response, next: NextFunction) => next());
      vi.mocked(db.gamificationRepo.awardAchievement).mockRejectedValue(new ForeignKeyConstraintError('User not found'));

      const response = await supertest(adminApp).post('/api/achievements/award').send({ userId: 'non-existent', achievementName: 'Test Award' });
      expect(response.status).toBe(400);
      expect(response.body.message).toBe('User not found');
    });
  });

  describe('GET /leaderboard', () => {
    it('should return a list of top users (public endpoint)', async () => {
      const mockLeaderboard = [createMockLeaderboardUser({ user_id: 'user-1', full_name: 'Leader', points: 1000, rank: '1' })];
      vi.mocked(db.gamificationRepo.getLeaderboard).mockResolvedValue(mockLeaderboard);

      const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard?limit=5');

      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockLeaderboard);
      expect(db.gamificationRepo.getLeaderboard).toHaveBeenCalledWith(5, expect.anything());
    });

    it('should return 500 if the database call fails', async () => {
      vi.mocked(db.gamificationRepo.getLeaderboard).mockRejectedValue(new Error('DB Error'));
      const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('DB Error');
    });

    it('should return 400 for an invalid limit parameter', async () => {
      const response = await supertest(unauthenticatedApp).get('/api/achievements/leaderboard?limit=100');
      expect(response.status).toBe(400);
      expect(response.body.errors).toBeDefined();
      expect(response.body.errors[0].message).toMatch(/less than or equal to 50|Too big/i);
    });
  });
});
