// src/routes/admin.users.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes'; // This was a duplicate, fixed.
import { createMockUserProfile, createMockAdminUserView } from '../tests/utils/mockFactories';
import { UserProfile, Profile } from '../types';
import { NotFoundError } from '../services/db/errors.db';
import { createTestApp } from '../tests/utils/createTestApp';

const { mockLogger } = vi.hoisted(() => ({
  mockLogger: {
    info: vi.fn(),
    debug: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
    child: vi.fn().mockReturnThis(),
  },
}));

vi.mock('../services/db/index.db', () => ({
  adminRepo: {
    getAllUsers: vi.fn(),
    updateUserRole: vi.fn(),
  },
  userRepo: {
    findUserProfileById: vi.fn(),
    deleteUserById: vi.fn(),
  },
  flyerRepo: {},
  recipeRepo: {},
  personalizationRepo: {},
  notificationRepo: {},
}));

// Mock other dependencies that are not directly tested but are part of the adminRouter setup
vi.mock('../services/db/flyer.db');
vi.mock('../services/db/recipe.db');
vi.mock('../services/backgroundJobService');
vi.mock('../services/geocodingService.server');
vi.mock('../services/queueService.server');
vi.mock('@bull-board/api');
vi.mock('@bull-board/api/bullMQAdapter');
vi.mock('node:fs/promises');

// Fix: Mock ExpressAdapter as a class to allow `new ExpressAdapter()` to work.
vi.mock('@bull-board/express', () => ({
  ExpressAdapter: class {
    setBasePath = vi.fn();
    getRouter = vi.fn().mockReturnValue((req: Request, res: Response, next: NextFunction) => next());
  },
}));

// Mock the logger
vi.mock('../services/logger.server', () => ({
  logger: mockLogger,
}));

// Import the mocked repos to control them in tests
import { adminRepo, userRepo } from '../services/db/index.db';

// Mock the passport middleware
vi.mock('./passport.routes', () => ({
  default: {
    authenticate: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
      if (!req.user) return res.status(401).json({ message: 'Unauthorized' });
      next();
    }),
  },
  isAdmin: (req: Request, res: Response, next: NextFunction) => {
    const user = req.user as UserProfile | undefined;
    if (user && user.role === 'admin') next();
    else res.status(403).json({ message: 'Forbidden: Administrator access required.' });
  },
}));

describe('Admin User Management Routes (/api/admin/users)', () => {
  const adminId = '123e4567-e89b-12d3-a456-426614174000';
  const userId = '123e4567-e89b-12d3-a456-426614174001';
  const adminUser = createMockUserProfile({ role: 'admin', user_id: adminId });
  // Create a single app instance with an admin user for all tests in this suite.
  const app = createTestApp({ router: adminRouter, basePath: '/api/admin', authenticatedUser: adminUser });

  // Add a basic error handler to capture errors passed to next(err) and return JSON.
  // This prevents unhandled error crashes in tests and ensures we get the 500 response we expect.
  app.use((err: any, req: any, res: any, next: any) => {
    res.status(err.status || 500).json({ message: err.message, errors: err.errors });
  });

  beforeEach(() => {
    vi.clearAllMocks();
  });

  describe('GET /users', () => {
    it('should return a list of all users on success', async () => {
      // Use the mock factory to create consistent test data.
      const mockUsers = [
        createMockAdminUserView({ user_id: '1', email: 'user1@test.com', role: 'user' }),
        createMockAdminUserView({ user_id: '2', email: 'user2@test.com', role: 'admin' }),
      ];
      vi.mocked(adminRepo.getAllUsers).mockResolvedValue(mockUsers);
      const response = await supertest(app).get('/api/admin/users');
      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockUsers);
      expect(adminRepo.getAllUsers).toHaveBeenCalledTimes(1);
    });
  });

  describe('GET /users/:id', () => {
    it('should fetch a single user successfully', async () => {
      const mockUser = createMockUserProfile({ user_id: userId });
      vi.mocked(userRepo.findUserProfileById).mockResolvedValue(mockUser);
      const response = await supertest(app).get(`/api/admin/users/${userId}`);
      expect(response.status).toBe(200);
      expect(response.body).toEqual(mockUser);
      expect(userRepo.findUserProfileById).toHaveBeenCalledWith(userId, expect.any(Object));
    });

    it('should return 404 for a non-existent user', async () => {
      const missingId = '123e4567-e89b-12d3-a456-426614174999';
      vi.mocked(userRepo.findUserProfileById).mockRejectedValue(new NotFoundError('User not found.'));
      const response = await supertest(app).get(`/api/admin/users/${missingId}`);
      expect(response.status).toBe(404);
      expect(response.body.message).toBe('User not found.');
    });
  });

  describe('PUT /users/:id', () => {
    it('should update a user role successfully', async () => {
      // The `updateUserRole` function returns a `Profile` object, not a `User` object.
      const updatedUser: Profile = {
        user_id: userId,
        role: 'admin',
        points: 0,
      };
      vi.mocked(adminRepo.updateUserRole).mockResolvedValue(updatedUser);
      const response = await supertest(app)
        .put(`/api/admin/users/${userId}`)
        .send({ role: 'admin' });
      expect(response.status).toBe(200);
      expect(response.body).toEqual(updatedUser);
      expect(adminRepo.updateUserRole).toHaveBeenCalledWith(userId, 'admin', expect.any(Object));
    });

    it('should return 404 for a non-existent user', async () => {
      const missingId = '123e4567-e89b-12d3-a456-426614174999';
      vi.mocked(adminRepo.updateUserRole).mockRejectedValue(new NotFoundError(`User with ID ${missingId} not found.`));
      const response = await supertest(app).put(`/api/admin/users/${missingId}`).send({ role: 'user' });
      expect(response.status).toBe(404);
      expect(response.body.message).toBe(`User with ID ${missingId} not found.`);
    });

    it('should return 400 for an invalid role', async () => {
      const response = await supertest(app)
        .put(`/api/admin/users/${userId}`)
        .send({ role: 'super-admin' });
      expect(response.status).toBe(400);
    });
  });

  describe('DELETE /users/:id', () => {
    it('should successfully delete a user', async () => {
      const targetId = '123e4567-e89b-12d3-a456-426614174999';
      vi.mocked(userRepo.deleteUserById).mockResolvedValue(undefined);
      const response = await supertest(app).delete(`/api/admin/users/${targetId}`);
      expect(response.status).toBe(204);
      expect(userRepo.deleteUserById).toHaveBeenCalledWith(targetId, expect.any(Object));
    });

    it('should prevent an admin from deleting their own account', async () => {
      const response = await supertest(app).delete(`/api/admin/users/${adminId}`);
      expect(response.status).toBe(400);
      expect(response.body.message).toMatch(/Admins cannot delete their own account/);
      expect(userRepo.deleteUserById).not.toHaveBeenCalled();
    });
  });
});