// src/routes/admin.jobs.routes.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import supertest from 'supertest';
import type { Request, Response, NextFunction } from 'express';
import adminRouter from './admin.routes';
import { createMockUserProfile } from '../tests/utils/mockFactories';
import type { Job } from 'bullmq';
import type { UserProfile } from '../types';
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(),
  },
}));

// --- Mocks ---

// Mock the background job service to control its methods.
vi.mock('../services/backgroundJobService', () => ({
  backgroundJobService: {
    runDailyDealCheck: vi.fn(),
  }
}));

// Mock the queue service and other dependencies of admin.routes.ts
vi.mock('../services/queueService.server', () => ({
  flyerQueue: { name: 'flyer-processing', add: vi.fn(), getJob: vi.fn() },
  emailQueue: { name: 'email-sending', add: vi.fn(), getJob: vi.fn() },
  analyticsQueue: { name: 'analytics-reporting', add: vi.fn(), getJob: vi.fn() },
  cleanupQueue: { name: 'file-cleanup', add: vi.fn(), getJob: vi.fn() },
  weeklyAnalyticsQueue: { name: 'weekly-analytics-reporting', add: vi.fn(), getJob: vi.fn() },
  flyerWorker: {},
  emailWorker: {},
  analyticsWorker: {},
  cleanupWorker: {},
  weeklyAnalyticsWorker: {},
}));
vi.mock('../services/db/index.db', () => ({
  adminRepo: {},
  flyerRepo: {},
  recipeRepo: {},
  userRepo: {},
  personalizationRepo: {},
  notificationRepo: {},
}));
vi.mock('../services/geocodingService.server');
vi.mock('node:fs/promises');

// Mock Bull Board UI dependencies
vi.mock('@bull-board/api');
vi.mock('@bull-board/api/bullMQAdapter');

// 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());
  },
}));

// Import the mocked modules to control them
import { backgroundJobService } from '../services/backgroundJobService'; // This is now a mock
import { flyerQueue, analyticsQueue, cleanupQueue } from '../services/queueService.server';

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

// 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 Job Trigger Routes (/api/admin/trigger)', () => {
  const adminUser = createMockUserProfile({ role: 'admin' });
  // 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('POST /trigger/daily-deal-check', () => {
    it('should trigger the daily deal check job and return 202 Accepted', async () => {
      // Use the instance method mock
      const response = await supertest(app).post('/api/admin/trigger/daily-deal-check');
      expect(response.status).toBe(202);
      expect(response.body.message).toContain('Daily deal check job has been triggered');
      expect(backgroundJobService.runDailyDealCheck).toHaveBeenCalledTimes(1);
    });

    it('should return 500 if triggering the job fails', async () => {
      vi.mocked(backgroundJobService.runDailyDealCheck).mockImplementation(() => { throw new Error('Job runner failed'); });
      const response = await supertest(app).post('/api/admin/trigger/daily-deal-check');
      expect(response.status).toBe(500);
      expect(response.body.message).toContain('Job runner failed');
    });
  });

  describe('POST /trigger/failing-job', () => {
    it('should enqueue a job designed to fail and return 202 Accepted', async () => {
      const mockJob = { id: 'failing-job-id-456' } as Job;
      vi.mocked(analyticsQueue.add).mockResolvedValue(mockJob);
      const response = await supertest(app).post('/api/admin/trigger/failing-job');
      expect(response.status).toBe(202);
      expect(response.body.message).toContain('Failing test job has been enqueued');
      expect(analyticsQueue.add).toHaveBeenCalledWith('generate-daily-report', { reportDate: 'FAIL' });
    });

    it('should return 500 if enqueuing the job fails', async () => {
      vi.mocked(analyticsQueue.add).mockRejectedValue(new Error('Queue is down'));
      const response = await supertest(app).post('/api/admin/trigger/failing-job');
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('Queue is down');
    });
  });

  describe('POST /flyers/:flyerId/cleanup', () => {
    it('should enqueue a cleanup job for a valid flyer ID', async () => {
      const flyerId = 789;
      const mockJob = { id: `cleanup-job-${flyerId}` } as Job;
      vi.mocked(cleanupQueue.add).mockResolvedValue(mockJob);
      const response = await supertest(app).post(`/api/admin/flyers/${flyerId}/cleanup`);
      expect(response.status).toBe(202);
      expect(response.body.message).toBe(`File cleanup job for flyer ID ${flyerId} has been enqueued.`);
      expect(cleanupQueue.add).toHaveBeenCalledWith('cleanup-flyer-files', { flyerId });
    });

    it('should return 500 if enqueuing the cleanup job fails', async () => {
      const flyerId = 789;
      vi.mocked(cleanupQueue.add).mockRejectedValue(new Error('Queue is down'));
      const response = await supertest(app).post(`/api/admin/flyers/${flyerId}/cleanup`);
      expect(response.status).toBe(500);
      expect(response.body.message).toBe('Queue is down');
    });

    it('should return 400 for an invalid flyerId', async () => {
      const response = await supertest(app).post('/api/admin/flyers/abc/cleanup');
      expect(response.status).toBe(400);
      expect(response.body.errors[0].message).toMatch(/Expected number, received nan/i);
    });
  });

  describe('POST /jobs/:queueName/:jobId/retry', () => {
    const queueName = 'flyer-processing';
    const jobId = 'failed-job-1';

    it('should successfully retry a failed job', async () => {
      // Arrange
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('failed'),
        retry: vi.fn().mockResolvedValue(undefined),
      };
      vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);

      // Act
      const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);

      // Assert
      expect(response.status).toBe(200);
      expect(response.body.message).toBe(`Job ${jobId} has been successfully marked for retry.`);
      expect(mockJob.retry).toHaveBeenCalledTimes(1);
    });

    it('should return 400 if the queue name is invalid', async () => {
      const response = await supertest(app).post(`/api/admin/jobs/invalid-queue/${jobId}/retry`);
      // Zod validation fails because queue name is an enum
      expect(response.status).toBe(400);
    });

    it('should return 404 if the job ID is not found in the queue', async () => {
      vi.mocked(flyerQueue.getJob).mockResolvedValue(undefined);
      const response = await supertest(app).post(`/api/admin/jobs/${queueName}/not-found-job/retry`);
      expect(response.status).toBe(404);
      expect(response.body.message).toContain('not found in queue');
    });

    it('should return 400 if the job is not in a failed state', async () => {
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('completed'),
        retry: vi.fn(),
      };
      vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);

      const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);

      expect(response.status).toBe(400);
      expect(response.body.message).toBe("Job is not in a 'failed' state. Current state: completed."); // This is now handled by the errorHandler
      expect(mockJob.retry).not.toHaveBeenCalled();
    });

    it('should return 500 if job.retry() throws an error', async () => {
      const mockJob = {
        id: jobId,
        getState: vi.fn().mockResolvedValue('failed'),
        retry: vi.fn().mockRejectedValue(new Error('Cannot retry job')),
      };
      vi.mocked(flyerQueue.getJob).mockResolvedValue(mockJob as unknown as Job);

      const response = await supertest(app).post(`/api/admin/jobs/${queueName}/${jobId}/retry`);

      expect(response.status).toBe(500);
      expect(response.body.message).toContain('Cannot retry job');
    });

    it('should return 400 for an invalid queueName or jobId', async () => {
      // This tests the Zod schema validation for the route params.
      const response = await supertest(app).post('/api/admin/jobs/ / /retry');
      expect(response.status).toBe(400);
    });
  });
});