import { ZoomMeetingAnalyticsProvider } from './zoom-meeting-analytics.provider';
import { ZoomAnalyticsResourceType } from 'src/common/enum/zoom-analytics-resource-type.enum';
import { OnlineTypeEnum } from 'src/common/enum/online-type.enum';
import { ZoomLiveEventType } from 'src/common/enum/zoom-live-event-type.enum';
import InifniBadRequestException from 'src/common/exceptions/infini-badrequest-exception';

describe('ZoomMeetingAnalyticsProvider', () => {
  const registry = { register: jest.fn() };
  const webinarRepository = { findByExtId: jest.fn(), findByExtIdForOccurrence: jest.fn(), save: jest.fn() };
  const reportApiClient = { fetchMeetingReport: jest.fn() };
  const rosterRepository = { findRegistrantRosterByOnlineSession: jest.fn() };
  const sessionSummaryRepository = { upsert: jest.fn(), findBySessionId: jest.fn() };
  const attendeeSummaryRepository = {
    upsertMany: jest.fn(),
    listBySession: jest.fn(),
    findAllBySessionId: jest.fn(),
  };
  const liveEventRepository = {
    create: jest.fn((data) => data),
    insert: jest.fn(),
    findAllByOnlineSessionId: jest.fn(),
  };
  const config = { getLateJoinThresholdSeconds: jest.fn().mockReturnValue(300) };
  const logger = { log: jest.fn(), warn: jest.fn(), error: jest.fn() };
  const onlineAttendanceService = {
    getManualMarksBySession: jest.fn(),
    getAttendedRegistrationIds: jest.fn(),
    recordZoomJoin: jest.fn(),
  };
  const registrationRepository = {
    findActiveExtensionJoinUrls: jest.fn(),
    findProfileImagesByIds: jest.fn(),
    findRmContactsByIds: jest.fn(),
  };
  const generatedLinkRepository = { findActiveMapBySession: jest.fn() };

  let provider: ZoomMeetingAnalyticsProvider;

  const onlineSession: any = { id: 80, externalId: 'ext-meeting-1', occurrenceId: null, actualMeetingEndsAt: null };
  const session = {
    id: 8,
    programId: 3,
    onlineType: OnlineTypeEnum.MEETING,
    onlineSession,
    startsAt: new Date('2026-07-08T10:00:00.000Z'),
  } as any;

  beforeEach(() => {
    jest.clearAllMocks();
    sessionSummaryRepository.findBySessionId.mockResolvedValue(null);
    config.getLateJoinThresholdSeconds.mockReturnValue(300);
    onlineAttendanceService.getManualMarksBySession.mockResolvedValue([]);
    onlineAttendanceService.getAttendedRegistrationIds.mockResolvedValue(new Set());
    registrationRepository.findActiveExtensionJoinUrls.mockResolvedValue(new Map());
    registrationRepository.findProfileImagesByIds.mockResolvedValue(new Map());
    registrationRepository.findRmContactsByIds.mockResolvedValue(new Map());
    attendeeSummaryRepository.findAllBySessionId.mockResolvedValue([]);
    generatedLinkRepository.findActiveMapBySession.mockResolvedValue(new Map());
    webinarRepository.save.mockResolvedValue(session);
    onlineSession.actualMeetingEndsAt = null;
    provider = new ZoomMeetingAnalyticsProvider(
      registry as any,
      webinarRepository as any,
      reportApiClient as any,
      rosterRepository as any,
      sessionSummaryRepository as any,
      attendeeSummaryRepository as any,
      liveEventRepository as any,
      config as any,
      logger as any,
      onlineAttendanceService as any,
      registrationRepository as any,
      generatedLinkRepository as any,
    );
  });

  it('self-registers under MEETING on module init', () => {
    provider.onModuleInit();
    expect(registry.register).toHaveBeenCalledWith(ZoomAnalyticsResourceType.MEETING, provider);
  });

  describe('reconcile', () => {
    it('refuses a non-meeting session (onlineType guard)', async () => {
      const webinarSession = { ...session, onlineType: OnlineTypeEnum.WEBINAR };

      await expect(provider.reconcile(webinarSession)).rejects.toBeInstanceOf(InifniBadRequestException);
      expect(reportApiClient.fetchMeetingReport).not.toHaveBeenCalled();
    });

    it('pulls the meeting report (not the webinar report) and the registrant roster (not the panelist roster)', async () => {
      liveEventRepository.findAllByOnlineSessionId.mockResolvedValue([]);
      rosterRepository.findRegistrantRosterByOnlineSession.mockResolvedValue([]);
      reportApiClient.fetchMeetingReport.mockResolvedValue({
        start_time: '2026-07-08T10:00:00.000Z',
        duration: 60,
      });

      await provider.reconcile(session);

      expect(reportApiClient.fetchMeetingReport).toHaveBeenCalledWith('ext-meeting-1');
      expect(rosterRepository.findRegistrantRosterByOnlineSession).toHaveBeenCalledWith(80);

      const summary = sessionSummaryRepository.upsert.mock.calls[0][0];
      expect(summary.resourceType).toBe(ZoomAnalyticsResourceType.MEETING);
      expect(summary.reconciledAt).toBeInstanceOf(Date);
    });

    it('computes attendee/session KPIs from the registrant roster + live event log, same as webinar', async () => {
      reportApiClient.fetchMeetingReport.mockResolvedValue({
        start_time: '2026-07-08T10:00:00.000Z',
        duration: 60,
      });
      liveEventRepository.findAllByOnlineSessionId.mockResolvedValue([
        {
          email: 'joined+reg1@x.com',
          eventType: ZoomLiveEventType.JOINED,
          occurredAt: new Date('2026-07-08T10:01:00.000Z'),
        },
      ]);
      rosterRepository.findRegistrantRosterByOnlineSession.mockResolvedValue([
        { registrationId: 1, userId: 11, fullName: 'Joined', email: 'joined@x.com', mobile: '111' },
        { registrationId: 2, userId: 12, fullName: 'Absent', email: 'absent@x.com', mobile: '222' },
      ]);

      await provider.reconcile(session);

      const summary = sessionSummaryRepository.upsert.mock.calls[0][0];
      expect(summary.totalPanelists).toBe(2);
      expect(summary.totalJoined).toBe(1);
      expect(summary.notJoined).toBe(1);
    });
  });

  describe('getKpis', () => {
    it('reads the registrant roster (not panelist roster) for a meeting session', async () => {
      rosterRepository.findRegistrantRosterByOnlineSession.mockResolvedValue([
        { registrationId: 1, userId: 11, fullName: 'A', email: 'a@x.com', mobile: '111' },
      ]);
      liveEventRepository.findAllByOnlineSessionId.mockResolvedValue([
        { email: 'a+reg1@x.com', eventType: ZoomLiveEventType.JOINED, occurredAt: new Date('2026-07-08T10:01:00Z') },
      ]);
      sessionSummaryRepository.findBySessionId.mockResolvedValue(null);

      const kpis = await provider.getKpis(session);

      expect(rosterRepository.findRegistrantRosterByOnlineSession).toHaveBeenCalledWith(80);
      expect(kpis.totalPanelists).toBe(1);
      expect(kpis.totalSeekersJoined).toBe(1);
    });

    it('returns all zeros when the meeting session has no online session at all', async () => {
      const bareSession = { id: 19, onlineType: OnlineTypeEnum.MEETING, startsAt: session.startsAt, onlineSession: null } as any;
      sessionSummaryRepository.findBySessionId.mockResolvedValue(null);

      const kpis = await provider.getKpis(bareSession);

      expect(kpis.totalPanelists).toBe(0);
      expect(rosterRepository.findRegistrantRosterByOnlineSession).not.toHaveBeenCalled();
    });
  });

  describe('getAttendeeTable', () => {
    it('syncs every registrant from the live event log using the meeting registrant roster', async () => {
      attendeeSummaryRepository.listBySession.mockResolvedValue({ data: [], total: 0 });
      rosterRepository.findRegistrantRosterByOnlineSession.mockResolvedValue([
        { registrationId: 1, userId: 11, fullName: 'A', email: 'a@x.com', mobile: '111' },
      ]);
      liveEventRepository.findAllByOnlineSessionId.mockResolvedValue([
        { email: 'a+reg1@x.com', eventType: ZoomLiveEventType.JOINED, occurredAt: new Date('2026-07-08T10:01:00Z') },
      ]);

      await provider.getAttendeeTable(session, { page: 1, limit: 20 } as any);

      expect(rosterRepository.findRegistrantRosterByOnlineSession).toHaveBeenCalledWith(80);
      expect(attendeeSummaryRepository.upsertMany).toHaveBeenCalledWith(
        expect.arrayContaining([expect.objectContaining({ email: 'a@x.com', isSystemAttended: true })]),
      );
    });
  });

  describe('recordParticipantJoined', () => {
    it('resolves user_id via the meeting registrant roster (not the panelist roster)', async () => {
      webinarRepository.findByExtIdForOccurrence.mockResolvedValue(session);
      rosterRepository.findRegistrantRosterByOnlineSession.mockResolvedValue([
        { registrationId: 1, userId: 42, fullName: 'A', email: 'a@x.com', mobile: '111' },
      ]);

      const occurredAt = new Date();
      await provider.recordParticipantJoined('ext-meeting-1', null, 'a+reg1@x.com', occurredAt, 'p-1', null);

      expect(rosterRepository.findRegistrantRosterByOnlineSession).toHaveBeenCalledWith(80);
      expect(liveEventRepository.insert).toHaveBeenCalledWith(
        expect.objectContaining({ userId: 42, eventType: ZoomLiveEventType.JOINED }),
      );
      expect(onlineAttendanceService.recordZoomJoin).toHaveBeenCalledWith({
        sessionId: 8,
        programId: 3,
        registrationId: 1,
        userId: 42,
        fullName: 'A',
        email: 'a@x.com',
        mobile: '111',
        occurredAt,
      });
    });
  });

  describe('markStarted / markEnded', () => {
    it('stamps resourceType MEETING (not WEBINAR) on the session summary', async () => {
      sessionSummaryRepository.findBySessionId.mockResolvedValue(null);

      await provider.markStarted(session, new Date('2026-07-08T10:00:00.000Z'));

      expect(sessionSummaryRepository.upsert).toHaveBeenCalledWith(
        expect.objectContaining({ resourceType: ZoomAnalyticsResourceType.MEETING }),
      );
    });

    it('markEnded stamps hdb_online_session.actual_meeting_ends_at for a meeting session too (base-class behavior, not webinar-specific)', async () => {
      sessionSummaryRepository.findBySessionId.mockResolvedValue(null);
      const endedAt = new Date('2026-07-08T11:00:00.000Z');

      await provider.markEnded(session, endedAt);

      expect(onlineSession.actualMeetingEndsAt).toEqual(endedAt);
      expect(webinarRepository.save).toHaveBeenCalledWith(session);
    });
  });
});
