import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { CommunicationMergeDataService } from './communication-merge-data.service';
import { SESSION_GUIDELINES_PDF_SERVICE } from './session-guidelines-pdf.types';
import { CommunicationTemplatesRepository } from '../repositories/communication-templates.repository';
import { MergeInfoAnswerLocationMap } from 'src/common/entities';
import { CommunicationTemplateAccessKeyEnum } from 'src/common/enum/communication-template-access-key.enum';
import { CommunicationTypeEnum } from 'src/common/enum/communication-type.enum';

/**
 * Covers the registration-less merge-field override that lets a template built for real
 * registrations (e.g. WELCOME_EMAIL_SEEKER, whose reg_name/zoom_join_link/meeting_id/
 * meeting_passcode fields are wired to a registrationId) also serve registration-less recipients
 * (general-link/common-invite) — reading common_<keyName> from extraMergeContext instead —
 * without any change to the template's own merge_field_map, and without touching real
 * registration-based sends.
 */
describe('CommunicationMergeDataService — registration-less merge-field override', () => {
  let service: CommunicationMergeDataService;
  let templatesRepo: { findByProgramAndAccessKey: jest.Mock; getTemplateIdBasedOnEnvironment: jest.Mock };
  let mergeInfoRepo: { find: jest.Mock };

  const accessKey = CommunicationTemplateAccessKeyEnum.WELCOME_EMAIL_SEEKER;

  const template = {
    id: 1,
    templateId: 'tpl-1',
    templateKey: 'KEY',
    templateType: CommunicationTypeEnum.EMAIL,
    masterTemplateId: null,
    step: null,
    attachedSteps: [],
  };

  // The real WELCOME_EMAIL_SEEKER-style identity fields: reg_name is a plain per-registration DB
  // column; zoom_join_link/meeting_id/meeting_passcode are registrationId-keyed computed fields.
  const identityFields = [
    {
      keyName: 'reg_name',
      sourceTable: 'hdb_program_registration',
      sourceColumn: 'full_name',
      isCommon: false,
      dataType: 'string',
      isNullable: false,
      defaultValue: null,
    },
    {
      keyName: 'zoom_join_link',
      sourceTable: 'computed',
      sourceColumn: 'getTargetOnlineSessionField:join_url',
      isCommon: true,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
    {
      keyName: 'meeting_id',
      sourceTable: 'computed',
      sourceColumn: 'getTargetOnlineSessionField:external_id',
      isCommon: true,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
    {
      keyName: 'meeting_passcode',
      sourceTable: 'computed',
      sourceColumn: 'getTargetOnlineSessionField:password',
      isCommon: true,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
  ];

  beforeEach(async () => {
    templatesRepo = {
      findByProgramAndAccessKey: jest.fn().mockResolvedValue(template),
      getTemplateIdBasedOnEnvironment: jest.fn().mockReturnValue('tpl-1'),
    };
    mergeInfoRepo = { find: jest.fn().mockResolvedValue(identityFields) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CommunicationMergeDataService,
        { provide: CommunicationTemplatesRepository, useValue: templatesRepo },
        { provide: getRepositoryToken(MergeInfoAnswerLocationMap), useValue: mergeInfoRepo },
        { provide: DataSource, useValue: { manager: {} } },
        {
          provide: SESSION_GUIDELINES_PDF_SERVICE,
          useValue: { resolveProgramPdfUrl: jest.fn(), resolveSessionPdfUrl: jest.fn() },
        },
      ],
    }).compile();

    service = module.get(CommunicationMergeDataService);
  });

  it('resolves reg_name/zoom_join_link/meeting_id/meeting_passcode from common_* context for a registration-less recipient', async () => {
    const result = await service.getTemplateWithMergeInfo(
      5,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      undefined, // no registrationId — a general-link/common-invite recipient
      undefined,
      {
        common_user_name: 'Jane Doe',
        common_zoom_join_link: 'https://zoom.us/j/999',
        common_meeting_id: '999',
        common_meeting_passcode: 'pass',
        sessionId: 9,
      },
      { resolveAllFieldsAsCommon: true },
    );

    expect(result?.mergeInfo).toMatchObject({
      reg_name: 'Jane Doe',
      zoom_join_link: 'https://zoom.us/j/999',
      meeting_id: '999',
      meeting_passcode: 'pass',
    });
  });

  it('leaves a real registration-based send unaffected — reg_name still resolves from the DB even with an incidental common_user_name in context', async () => {
    const queryBuilder: any = {
      select: jest.fn().mockReturnThis(),
      from: jest.fn().mockReturnThis(),
      where: jest.fn().mockReturnThis(),
      limit: jest.fn().mockReturnThis(),
      getSql: jest.fn().mockReturnValue('SELECT 1'),
      getRawOne: jest.fn().mockResolvedValue({ value: 'DB Name' }),
    };
    const manager = { createQueryBuilder: jest.fn().mockReturnValue(queryBuilder) };

    // Only reg_name (per-record, isCommon:false) is relevant here — the seeker WELCOME flow's
    // real call never sets resolveAllFieldsAsCommon, so it lands in processPerRecordMergeFields,
    // which this override does not touch.
    mergeInfoRepo.find.mockResolvedValue([identityFields[0]]);

    const result = await service.getTemplateWithMergeInfo(
      5,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      55, // real registrationId
      manager,
      { common_user_name: 'Jane Doe' }, // incidental — must not win
    );

    expect(result?.mergeInfo.reg_name).toBe('DB Name');
  });

  // ABSENT_REGULAR/FINAL_EMAIL_SEEKER's absentDataTabel (getAbsentSessionsTable) is normally
  // keyed by registrationId against program_user_attendance. A general-link ABSENT recipient has
  // no registration, so it must fall back to zoom_analytics_attendee_summary matched by
  // common_user_email — the same source getGeneralLinkRecipients itself uses for eligibility.
  describe('resolveAbsentSessions email fallback (GENERAL_LINK_ABSENT)', () => {
    const absentField = [
      {
        keyName: 'absentDataTabel',
        sourceTable: 'computed',
        sourceColumn: 'getAbsentSessionsTable',
        isCommon: false,
        dataType: 'string',
        isNullable: true,
        defaultValue: '',
      },
    ];

    it('builds the absent-sessions table from zoom_analytics_attendee_summary when there is no registrationId but common_user_email is present', async () => {
      mergeInfoRepo.find.mockResolvedValue(absentField);

      let call = 0;
      const manager = {
        createQueryBuilder: jest.fn().mockImplementation(() => {
          call += 1;
          const isCutoffLookup = call === 1; // first call resolves the target session's starts_at
          const qb: any = {
            select: jest.fn().mockReturnThis(),
            addSelect: jest.fn().mockReturnThis(),
            from: jest.fn().mockReturnThis(),
            where: jest.fn().mockReturnThis(),
            andWhere: jest.fn().mockReturnThis(),
            orderBy: jest.fn().mockReturnThis(),
          };
          if (isCutoffLookup) {
            qb.getRawOne = jest.fn().mockResolvedValue({ startsAt: '2026-07-27T09:55:00Z' });
          } else {
            qb.getRawMany = jest
              .fn()
              .mockResolvedValue([{ name: 'Session One', startsAt: '2026-07-13T06:00:00Z' }]);
          }
          return qb;
        }),
      };

      const result = await service.getTemplateWithMergeInfo(
        5,
        accessKey,
        CommunicationTypeEnum.EMAIL,
        undefined,
        manager,
        { common_user_email: 'staff@example.com', sessionId: 1800 },
        { resolveAllFieldsAsCommon: true },
      );

      expect(result?.mergeInfo.absentDataTabel).toContain('Session One');
    });

    it('returns an empty absentDataTabel (no query at all) when neither registrationId nor common_user_email is present', async () => {
      mergeInfoRepo.find.mockResolvedValue(absentField);
      const manager = { createQueryBuilder: jest.fn() };

      const result = await service.getTemplateWithMergeInfo(
        5,
        accessKey,
        CommunicationTypeEnum.EMAIL,
        undefined,
        manager,
        {},
        { resolveAllFieldsAsCommon: true },
      );

      expect(result?.mergeInfo.absentDataTabel).toBe('');
      expect(manager.createQueryBuilder).not.toHaveBeenCalled();
    });
  });
});
