import { Test, TestingModule } from '@nestjs/testing';
import { ForbiddenException } from '@nestjs/common';
import { AnalyticsController } from './analytics.controller';
import { ZoomAnalyticsFacadeService } from 'src/zoom/services/zoom-analytics-facade.service';
import { ResponseService } from 'src/common/response-handling/response-handler';
import { CombinedAuthGuard } from 'src/auth/combined-auth.guard';
import { RolesGuard } from 'src/common/guards/roles.guard';
import { ZoomAttendanceMarkSource } from 'src/common/enum/zoom-attendance-mark-source.enum';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';
import { ERROR_CODES } from 'src/common/constants/error-string-constants';
import { ROLE_GUARD_STRINGS } from 'src/common/constants/strings-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, roles: string[] = ['admin']) => ({ user: { id: userId, roles } });

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

  const mockFacade = {
    getKpis: jest.fn(),
    getLiveStatus: jest.fn(),
    getAttendeeTable: jest.fn(),
    reconcile: jest.fn(),
    markAttendance: jest.fn(),
    getDashboard: jest.fn(),
    getFollowUps: jest.fn(),
  };

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

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

    const module: TestingModule = await Test.createTestingModule({
      controllers: [AnalyticsController],
      providers: [
        { provide: ZoomAnalyticsFacadeService, useValue: mockFacade },
        { provide: ResponseService, useValue: mockResponseService },
      ],
    })
      .overrideGuard(CombinedAuthGuard)
      .useValue({ canActivate: () => true })
      .overrideGuard(RolesGuard)
      .useValue({ canActivate: () => true })
      .compile();

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

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

  describe('getKpis', () => {
    it('returns the 8 KPIs for the given session in one response', async () => {
      const res = makeMockRes();
      const kpis = { sessionId: 7, totalPanelists: 10 };
      mockFacade.getKpis.mockResolvedValue(kpis);

      await controller.getKpis(7, makeMockReq() as any, res as any);

      expect(mockFacade.getKpis).toHaveBeenCalledWith(7, ['admin'], 1);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), kpis);
    });

    it('forwards the RM caller\'s active role and id to the facade for scoping', async () => {
      const res = makeMockRes();
      const kpis = { sessionId: 7, totalPanelists: 3 };
      mockFacade.getKpis.mockResolvedValue(kpis);

      await controller.getKpis(7, makeMockReq(42, ['relational_manager']) as any, res as any);

      expect(mockFacade.getKpis).toHaveBeenCalledWith(7, ['relational_manager'], 42);
    });

    it('calls handleControllerError when the session does not exist', async () => {
      const res = makeMockRes();
      mockFacade.getKpis.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_SESSION_NOTFOUND, null, null, '999'),
      );

      await controller.getKpis(999, makeMockReq() as any, res as any);

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

  describe('getLive', () => {
    it('returns the live snapshot for the session', async () => {
      const res = makeMockRes();
      const live = { sessionId: 7, isLive: true };
      mockFacade.getLiveStatus.mockResolvedValue(live);

      await controller.getLive(7, res as any);

      expect(mockFacade.getLiveStatus).toHaveBeenCalledWith(7);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), live);
    });
  });

  describe('getAttendees', () => {
    it('forwards pagination/search query to the facade', async () => {
      const res = makeMockRes();
      const query = { page: 2, limit: 10, search: 'ram' };
      const rows = { data: [], total: 0, page: 2, limit: 10 };
      mockFacade.getAttendeeTable.mockResolvedValue(rows);

      await controller.getAttendees(7, query as any, makeMockReq() as any, res as any);

      expect(mockFacade.getAttendeeTable).toHaveBeenCalledWith(7, query, ['admin'], 1);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), rows);
    });

    it('forwards the RM caller\'s active role and id to the facade for scoping', async () => {
      const res = makeMockRes();
      const query = { page: 1, limit: 10 };
      const rows = { data: [], total: 0, page: 1, limit: 10 };
      mockFacade.getAttendeeTable.mockResolvedValue(rows);

      await controller.getAttendees(7, query as any, makeMockReq(42, ['relational_manager']) as any, res as any);

      expect(mockFacade.getAttendeeTable).toHaveBeenCalledWith(7, query, ['relational_manager'], 42);
    });
  });

  describe('getDashboard', () => {
    it('returns the stored post-session dashboard for the session', async () => {
      const res = makeMockRes();
      const dashboard = { sessionId: 7, reconciledAt: null };
      mockFacade.getDashboard.mockResolvedValue(dashboard);

      await controller.getDashboard(7, res as any);

      expect(mockFacade.getDashboard).toHaveBeenCalledWith(7);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), dashboard);
    });

    it('calls handleControllerError when the session does not exist', async () => {
      const res = makeMockRes();
      mockFacade.getDashboard.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.PROGRAM_SESSION_NOTFOUND, null, null, '999'),
      );

      await controller.getDashboard(999, res as any);

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

  describe('getFollowUps', () => {
    it('forwards pagination/search query to the facade', async () => {
      const res = makeMockRes();
      const query = { page: 1, limit: 10, search: 'ram' };
      const rows = { data: [], total: 0, page: 1, limit: 10 };
      mockFacade.getFollowUps.mockResolvedValue(rows);

      await controller.getFollowUps(7, query as any, res as any);

      expect(mockFacade.getFollowUps).toHaveBeenCalledWith(7, query);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String), rows);
    });
  });

  describe('reconcile', () => {
    it('triggers reconciliation and returns success with no payload', async () => {
      const res = makeMockRes();
      mockFacade.reconcile.mockResolvedValue(undefined);

      await controller.reconcile(7, res as any);

      expect(mockFacade.reconcile).toHaveBeenCalledWith(7);
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String));
    });

    it('calls handleControllerError on reconcile failure', async () => {
      const res = makeMockRes();
      mockFacade.reconcile.mockRejectedValue(new Error('zoom api down'));

      await controller.reconcile(7, res as any);

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

  describe('markAttendance', () => {
    it('forwards markedBy and the actor\'s active roles from req.user to the facade', async () => {
      const res = makeMockRes();
      const dto = { source: ZoomAttendanceMarkSource.COORDINATOR, attended: true };
      mockFacade.markAttendance.mockResolvedValue(undefined);

      await controller.markAttendance(7, 42, dto as any, makeMockReq(9, ['shoba']) as any, res as any);

      expect(mockFacade.markAttendance).toHaveBeenCalledWith(
        7,
        42,
        ZoomAttendanceMarkSource.COORDINATOR,
        true,
        9,
        ['shoba'],
      );
      expect(mockResponseService.success).toHaveBeenCalledWith(res, expect.any(String));
    });

    it('calls handleControllerError when the attendee does not exist', async () => {
      const res = makeMockRes();
      const dto = { source: ZoomAttendanceMarkSource.RM, attended: true };
      mockFacade.markAttendance.mockRejectedValue(
        new InifniNotFoundException(ERROR_CODES.ZOOM_ANALYTICS_ATTENDEE_NOTFOUND, null, null, '999'),
      );

      await controller.markAttendance(7, 999, dto as any, makeMockReq() as any, res as any);

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

    it('calls handleControllerError with a forbidden response when the caller lacks the matching active role', async () => {
      const res = makeMockRes();
      const dto = { source: ZoomAttendanceMarkSource.RM, attended: true };
      mockFacade.markAttendance.mockRejectedValue(new ForbiddenException(ROLE_GUARD_STRINGS.UNAUTHORIZED));

      await controller.markAttendance(7, 42, dto as any, makeMockReq(9, ['admin']) as any, res as any);

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