import { ZoomFinalSessionConfirmService } from './zoom-final-session-confirm.service';
import { ZoomFinalSessionAttendanceService } from './zoom-final-session-attendance.service';
import { AttendanceStatus } from 'src/common/enum/attendance-status.enum';
import { ZoomRegistrationUpdate } from '../enums/zoom-registration-update.enum';
import { ZoomRole } from '../enums/zoom-role.enum';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';
import { InifniNotFoundException } from 'src/common/exceptions/infini-notfound-exception';

describe('ZoomFinalSessionConfirmService', () => {
  const past = (daysAgo: number) => new Date(Date.now() - daysAgo * 24 * 60 * 60 * 1000);
  const future = (daysAhead: number) => new Date(Date.now() + daysAhead * 24 * 60 * 60 * 1000);

  const session = (id: number, startsAt: Date, onlineSessionId: number | null = 100 + id) => ({
    id,
    programId: 1,
    startsAt,
    displayOrder: 1,
    onlineSession: onlineSessionId != null ? { id: onlineSessionId } : null,
  });

  const registration = (id: number, fullName = `Reg ${id}`, rmContact: number | null = null) => ({
    id,
    fullName,
    programId: 1,
    rmContact,
  });

  const programSessionRepo = { find: jest.fn(), save: jest.fn() };
  const registrationRepository = {
    listRegisteredExtensionsForSession: jest.fn(),
    findRegistrationById: jest.fn(),
  };
  const zoomRegistrationService = { handle: jest.fn() };
  const onlineAttendanceService = { getManualMarksBySession: jest.fn() };
  const logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };

  let service: ZoomFinalSessionConfirmService;

  beforeEach(() => {
    jest.clearAllMocks();
    // Real instance backed by the same mocks — exercises the actual shared attendance math
    // rather than re-mocking it, since ZoomFinalSessionConfirmService is a thin caller of it.
    const attendanceService = new ZoomFinalSessionAttendanceService(
      programSessionRepo as any,
      onlineAttendanceService as any,
    );
    service = new ZoomFinalSessionConfirmService(
      programSessionRepo as any,
      registrationRepository as any,
      zoomRegistrationService as any,
      attendanceService,
      logger as any,
    );
  });

  describe('confirmFinalSession', () => {
    it('rejects a program with a single session', async () => {
      programSessionRepo.find.mockResolvedValue([session(1, past(1))]);

      await expect(service.confirmFinalSession(1)).rejects.toBeInstanceOf(InifniBadRequestException);
      expect(registrationRepository.listRegisteredExtensionsForSession).not.toHaveBeenCalled();
    });

    it('rejects when the final session has no provisioned Zoom resource', async () => {
      programSessionRepo.find.mockResolvedValue([
        session(1, past(2)),
        session(2, past(1), null),
      ]);

      await expect(service.confirmFinalSession(1)).rejects.toBeInstanceOf(InifniBadRequestException);
    });

    it('unregisters a registrant absent from an earlier elapsed session, leaves an all-present registrant alone', async () => {
      programSessionRepo.find.mockResolvedValue([session(1, past(2)), session(2, past(1)), session(3, future(1))]);
      registrationRepository.listRegisteredExtensionsForSession.mockResolvedValue([
        { registration: registration(10, 'Absent Once') },
        { registration: registration(11, 'Always Present') },
      ]);
      onlineAttendanceService.getManualMarksBySession.mockImplementation((sessionId: number) => {
        if (sessionId === 1) {
          return Promise.resolve([
            { registrationId: 10, attendanceStatus: AttendanceStatus.ABSENT },
            { registrationId: 11, attendanceStatus: AttendanceStatus.PRESENT },
          ]);
        }
        return Promise.resolve([
          { registrationId: 10, attendanceStatus: AttendanceStatus.PRESENT },
          { registrationId: 11, attendanceStatus: AttendanceStatus.PRESENT },
        ]);
      });
      zoomRegistrationService.handle.mockResolvedValue(undefined);

      const result = await service.confirmFinalSession(1, 99);

      expect(onlineAttendanceService.getManualMarksBySession).toHaveBeenCalledTimes(2);
      expect(onlineAttendanceService.getManualMarksBySession).toHaveBeenCalledWith(1);
      expect(onlineAttendanceService.getManualMarksBySession).toHaveBeenCalledWith(2);
      // Session 3 (the final session) is never queried for attendance.
      expect(onlineAttendanceService.getManualMarksBySession).not.toHaveBeenCalledWith(3);

      expect(zoomRegistrationService.handle).toHaveBeenCalledTimes(1);
      // Blocks the final session only — restrictUnregisterToTargetSession keeps the earlier
      // (elapsed) sessions' registration rows intact instead of cascading the shared group.
      expect(zoomRegistrationService.handle).toHaveBeenCalledWith(
        {
          registrationId: 10,
          sessionId: 3,
          action: ZoomRegistrationUpdate.UNREGISTER,
          actingUserId: 99,
        },
        { restrictUnregisterToTargetSession: true },
      );

      expect(result).toEqual({
        programId: 1,
        finalSessionId: 3,
        totalEvaluated: 2,
        blockedCount: 1,
        blocked: [{ registrationId: 10, fullName: 'Absent Once', missedSessionIds: [1] }],
        failed: [],
      });

      expect(programSessionRepo.save).toHaveBeenCalledTimes(1);
      const saved = programSessionRepo.save.mock.calls[0][0];
      expect(saved.id).toBe(3);
      expect(saved.onlineSession.finalized).toBe(true);
    });

    it('only evaluates non-final sessions that have already elapsed', async () => {
      programSessionRepo.find.mockResolvedValue([session(1, future(1)), session(2, future(2))]);
      registrationRepository.listRegisteredExtensionsForSession.mockResolvedValue([
        { registration: registration(10) },
      ]);

      const result = await service.confirmFinalSession(1);

      expect(onlineAttendanceService.getManualMarksBySession).not.toHaveBeenCalled();
      expect(zoomRegistrationService.handle).not.toHaveBeenCalled();
      expect(result.blockedCount).toBe(0);
    });

    it('collects a failed unregister without aborting the rest of the run', async () => {
      programSessionRepo.find.mockResolvedValue([session(1, past(1)), session(2, future(1))]);
      registrationRepository.listRegisteredExtensionsForSession.mockResolvedValue([
        { registration: registration(10, 'First') },
        { registration: registration(11, 'Second') },
      ]);
      onlineAttendanceService.getManualMarksBySession.mockResolvedValue([]);
      zoomRegistrationService.handle
        .mockRejectedValueOnce(new Error('Zoom API down'))
        .mockResolvedValueOnce(undefined);

      const result = await service.confirmFinalSession(1);

      expect(zoomRegistrationService.handle).toHaveBeenCalledTimes(2);
      expect(result.blockedCount).toBe(1);
      expect(result.blocked).toEqual([{ registrationId: 11, fullName: 'Second', missedSessionIds: [1] }]);
      expect(result.failed).toEqual([{ registrationId: 10, error: 'Zoom API down' }]);
    });

    it('passes the RM scoping id through to the repository query', async () => {
      programSessionRepo.find.mockResolvedValue([session(1, past(1)), session(2, future(1))]);
      registrationRepository.listRegisteredExtensionsForSession.mockResolvedValue([]);

      await service.confirmFinalSession(1, 5, 42);

      expect(registrationRepository.listRegisteredExtensionsForSession).toHaveBeenCalledWith(102, 42);
    });
  });

  describe('reinstateForFinalSession', () => {
    it('rejects a registration id that does not exist', async () => {
      registrationRepository.findRegistrationById.mockResolvedValue(null);

      await expect(service.reinstateForFinalSession(10, undefined)).rejects.toBeInstanceOf(
        InifniNotFoundException,
      );
      expect(zoomRegistrationService.handle).not.toHaveBeenCalled();
    });

    it('rejects an RM caller acting on a registration that is not their own contact', async () => {
      registrationRepository.findRegistrationById.mockResolvedValue(registration(10, 'Reg 10', 7));

      await expect(service.reinstateForFinalSession(10, undefined, 1, 8)).rejects.toBeInstanceOf(
        InifniNotFoundException,
      );
      expect(zoomRegistrationService.handle).not.toHaveBeenCalled();
    });

    it('rejects when the registration program has only one session', async () => {
      registrationRepository.findRegistrationById.mockResolvedValue(registration(10));
      programSessionRepo.find.mockResolvedValue([session(1, past(1))]);

      await expect(service.reinstateForFinalSession(10, undefined)).rejects.toBeInstanceOf(
        InifniBadRequestException,
      );
    });

    it('re-registers the registrant against the current final session as an attendee', async () => {
      registrationRepository.findRegistrationById.mockResolvedValue(registration(10, 'Reg 10', 7));
      programSessionRepo.find.mockResolvedValue([session(1, past(2)), session(2, past(1))]);
      zoomRegistrationService.handle.mockResolvedValue({ id: 555 });

      const result = await service.reinstateForFinalSession(10, 'exception approved', 99, 7);

      expect(zoomRegistrationService.handle).toHaveBeenCalledWith({
        registrationId: 10,
        sessionId: 2,
        action: ZoomRegistrationUpdate.REGISTER,
        role: ZoomRole.ATTENDEE,
        actingUserId: 99,
      });
      expect(result).toEqual({ id: 555 });
    });
  });
});
