import { Test, TestingModule } from '@nestjs/testing';
import { HttpStatus } from '@nestjs/common';
import { QrAttendanceController } from './qr-attendance.controller';
import { QrAttendanceService } from './qr-attendance.service';
import { ResponseService } from 'src/common/response-handling/response-handler';
import { AppLoggerService } from 'src/common/services/logger.service';
import { CombinedAuthGuard } from 'src/auth/combined-auth.guard';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { ExportJobStatus } from 'src/common/enum/export-job-status.enum';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';

jest.mock('src/common/utils/controller-response-handling', () => ({
  handleControllerError: jest.fn((res, error) => res.json({ success: false, error })),
}));

const makeMockRes = () => ({
  json: jest.fn(),
  status: jest.fn().mockReturnThis(),
});

const makeMockReq = (userId = 1) => ({ user: { id: userId } });

describe('QrAttendanceController', () => {
  let controller: QrAttendanceController;

  const mockService = {
    generateQr: jest.fn(),
    startBulkGeneration: jest.fn(),
    getBulkJobStatus: jest.fn(),
    scanQr: jest.fn(),
    manualCheckin: jest.fn(),
    undoCheckin: jest.fn(),
    findById: jest.fn(),
    findList: jest.fn(),
  };

  const mockResponseService = {
    success: jest.fn((res, _msg, data, _status) => res.json({ success: true, data })),
  };

  const mockLogger = { log: jest.fn(), error: jest.fn(), warn: jest.fn() };

  beforeEach(async () => {
    jest.clearAllMocks();

    const module: TestingModule = await Test.createTestingModule({
      controllers: [QrAttendanceController],
      providers: [
        { provide: QrAttendanceService, useValue: mockService },
        { provide: ResponseService, useValue: mockResponseService },
        { provide: AppLoggerService, useValue: mockLogger },
      ],
    })
      .overrideGuard(CombinedAuthGuard)
      .useValue({ canActivate: () => true })
      .overrideGuard(RolesGuard)
      .useValue({ canActivate: () => true })
      .compile();

    controller = module.get<QrAttendanceController>(QrAttendanceController);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
  });

  // ─── POST /qr-attendance/generate ────────────────────────────────────────────

  describe('generateQr', () => {
    const dto = { registrationId: 10 } as any;
    const attendance = { id: 1, registrationId: 10, qrUrl: 'https://s3.example.com/qr.jpeg' };

    it('calls service.generateQr with createdBy from req.user and returns 201', async () => {
      const res = makeMockRes();
      mockService.generateQr.mockResolvedValue(attendance);

      await controller.generateQr(dto, makeMockReq(5), res as any);

      expect(mockService.generateQr).toHaveBeenCalledWith(
        expect.objectContaining({ registrationId: 10, createdBy: 5 }),
      );
      expect(mockResponseService.success).toHaveBeenCalledWith(
        res,
        expect.any(String),
        attendance,
        HttpStatus.CREATED,
      );
    });

    it('calls handleControllerError on service failure', async () => {
      const res = makeMockRes();
      mockService.generateQr.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_REGISTRATION_NOTFOUND, null, null, '10'),
      );

      await controller.generateQr(dto, makeMockReq(), res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── POST /qr-attendance/generate-bulk ───────────────────────────────────────

  describe('generateBulkQr', () => {
    const dto = { programId: 3, sessionId: 7, batchSize: 10 } as any;
    const jobResult = { jobId: 42, status: ExportJobStatus.PROCESSING, total: 15 };

    it('calls service.startBulkGeneration and returns 202 with jobId', async () => {
      const res = makeMockRes();
      mockService.startBulkGeneration.mockResolvedValue(jobResult);

      await controller.generateBulkQr(dto, res as any);

      expect(mockService.startBulkGeneration).toHaveBeenCalledWith(dto);
      expect(mockResponseService.success).toHaveBeenCalledWith(
        res,
        expect.any(String),
        jobResult,
        HttpStatus.ACCEPTED,
      );
    });

    it('calls handleControllerError on service failure', async () => {
      const res = makeMockRes();
      mockService.startBulkGeneration.mockRejectedValue(new Error('bulk error'));

      await controller.generateBulkQr(dto, res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── GET /qr-attendance/generate-bulk/status/:jobId ──────────────────────────

  describe('getBulkJobStatus', () => {
    const job = { id: 42, status: ExportJobStatus.COMPLETED, total: 15, generated: 15 };

    it('returns job status for given jobId', async () => {
      const res = makeMockRes();
      mockService.getBulkJobStatus.mockResolvedValue(job);

      await controller.getBulkJobStatus(42, res as any);

      expect(mockService.getBulkJobStatus).toHaveBeenCalledWith(42);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), job);
    });

    it('calls handleControllerError when job not found', async () => {
      const res = makeMockRes();
      mockService.getBulkJobStatus.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_BULK_JOB_NOTFOUND, null, null, '99'),
      );

      await controller.getBulkJobStatus(99, res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── POST /qr-attendance/scan ────────────────────────────────────────────────

  describe('scanQr', () => {
    const dto = { registrationId: 10 } as any;

    it('forwards checkedInByUserId from req.user and returns success message', async () => {
      const res = makeMockRes();
      const attended = { id: 1, isAttended: true, checkedInAt: new Date() };
      mockService.scanQr.mockResolvedValue(attended);

      await controller.scanQr(dto, makeMockReq(3), res as any);

      expect(mockService.scanQr).toHaveBeenCalledWith(
        expect.objectContaining({ registrationId: 10, checkedInByUserId: 3 }),
      );
      expect(mockResponseService.success).toHaveBeenCalled();
    });

    it('returns ALREADY_ATTENDED message when already checked in', async () => {
      const res = makeMockRes();
      const alreadyAttended = { id: 1, isAttended: true, checkedInAt: null };
      mockService.scanQr.mockResolvedValue(alreadyAttended);

      await controller.scanQr(dto, makeMockReq(), res as any);

      expect(mockResponseService.success).toHaveBeenCalled();
    });

    it('calls handleControllerError for invalid QR token', async () => {
      const res = makeMockRes();
      mockService.scanQr.mockRejectedValue(
        new InifniBadRequestException(ERROR_CODES.PROGRAM_ATTENDANCE_INVALID_QR_TOKEN),
      );

      await controller.scanQr(dto, makeMockReq(), res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── POST /qr-attendance/manual-checkin ──────────────────────────────────────

  describe('manualCheckin', () => {
    const dto = { registrationId: 10, sessionId: 7 } as any;

    it('forwards checkedInByUserId from req.user and saves', async () => {
      const res = makeMockRes();
      const saved = { id: 1, isManuallyCheckedIn: true, isAttended: true };
      mockService.manualCheckin.mockResolvedValue(saved);

      await controller.manualCheckin(dto, makeMockReq(4), res as any);

      expect(mockService.manualCheckin).toHaveBeenCalledWith(
        expect.objectContaining({ registrationId: 10, checkedInByUserId: 4 }),
      );
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), saved);
    });

    it('calls handleControllerError when attendance not found', async () => {
      const res = makeMockRes();
      mockService.manualCheckin.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_NOTFOUND, null, null, '10'),
      );

      await controller.manualCheckin(dto, makeMockReq(), res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── POST /qr-attendance/undo-checkin ────────────────────────────────────────

  describe('undoCheckin', () => {
    const dto = { attendanceIds: [1, 2, 3] } as any;

    it('calls service.undoCheckin and returns success', async () => {
      const res = makeMockRes();
      mockService.undoCheckin.mockResolvedValue(undefined);

      await controller.undoCheckin(dto, res as any);

      expect(mockService.undoCheckin).toHaveBeenCalledWith(dto);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String));
    });

    it('calls handleControllerError on service failure', async () => {
      const res = makeMockRes();
      mockService.undoCheckin.mockRejectedValue(new Error('undo failed'));

      await controller.undoCheckin(dto, res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── GET /qr-attendance ───────────────────────────────────────────────────────

  describe('findList', () => {
    const query = { programId: 3, limit: 20, offset: 0 } as any;

    it('returns paginated attendance list', async () => {
      const res = makeMockRes();
      const listResult = {
        data: [{ id: 1 }, { id: 2 }],
        total: 2,
        limit: 20,
        offset: 0,
        statusCounts: [],
      };
      mockService.findList.mockResolvedValue(listResult);

      await controller.findList(query, res as any);

      expect(mockService.findList).toHaveBeenCalledWith(query);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), listResult);
    });

    it('calls handleControllerError on service failure', async () => {
      const res = makeMockRes();
      mockService.findList.mockRejectedValue(new Error('db error'));

      await controller.findList(query, res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });

  // ─── GET /qr-attendance/:id ───────────────────────────────────────────────────

  describe('findById', () => {
    it('returns attendance record for given id', async () => {
      const res = makeMockRes();
      const attendance = { id: 1, registrationId: 10 };
      mockService.findById.mockResolvedValue(attendance);

      await controller.findById(1, res as any);

      expect(mockService.findById).toHaveBeenCalledWith(1);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), attendance);
    });

    it('calls handleControllerError when record not found', async () => {
      const res = makeMockRes();
      mockService.findById.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_ATTENDANCE_NOTFOUND, null, null, '99'),
      );

      await controller.findById(99, res as any);

      expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ success: false }));
    });
  });
});
