import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import {
  CommunicationMergeDataService,
  createSendMergeCache,
} 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 per-send resolution cache (SendMergeCache): a bulk send calls
 * getTemplateWithMergeInfo once per recipient, and everything that is constant across the send —
 * the template row, its merge-field map, and every `isCommon` field — must resolve exactly once.
 *
 * The guidelines PDF is the field this matters most for: each resolve is a DB read plus a fresh
 * S3 presign, and on a cache miss it renders the PDF through puppeteer. Repeating that per
 * recipient is what made large sends slow.
 */
describe('CommunicationMergeDataService — per-send merge cache', () => {
  let service: CommunicationMergeDataService;
  let templatesRepo: {
    findByProgramAndAccessKey: jest.Mock;
    getTemplateIdBasedOnEnvironment: jest.Mock;
  };
  let mergeInfoRepo: { find: jest.Mock };
  let guidelinesPdf: { resolveProgramPdfUrl: jest.Mock; resolveSessionPdfUrl: 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: [],
  };

  /** guideline_pdf is the expensive send-constant field; reg_name is the per-recipient one. */
  const fields = [
    {
      keyName: 'guideline_pdf',
      sourceTable: 'computed',
      sourceColumn: 'getWelcomeGuidelinesPdfUrl',
      isCommon: true,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
    {
      keyName: 'reg_name',
      sourceTable: 'hdb_program_registration',
      sourceColumn: 'full_name',
      isCommon: false,
      dataType: 'string',
      isNullable: false,
      defaultValue: null,
    },
  ];

  const buildManager = (name: string) => {
    const queryBuilder: any = {
      select: jest.fn().mockReturnThis(),
      addSelect: 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: name, userId: 1, programId: 5 }),
    };
    return { createQueryBuilder: jest.fn().mockReturnValue(queryBuilder) };
  };

  beforeEach(async () => {
    templatesRepo = {
      findByProgramAndAccessKey: jest.fn().mockResolvedValue(template),
      getTemplateIdBasedOnEnvironment: jest.fn().mockReturnValue('tpl-1'),
    };
    mergeInfoRepo = { find: jest.fn().mockResolvedValue(fields) };
    guidelinesPdf = {
      resolveProgramPdfUrl: jest.fn().mockResolvedValue('https://s3/guidelines.pdf?signed=1'),
      resolveSessionPdfUrl: jest.fn(),
    };

    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: guidelinesPdf },
      ],
    }).compile();

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

  const resolveFor = (registrationId: number, manager: any, cache?: any) =>
    service.getTemplateWithMergeInfo(
      5,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      registrationId,
      manager,
      {},
      cache ? { cache } : undefined,
    );

  it('resolves the template, its merge map and the guidelines PDF once across a batch', async () => {
    const cache = createSendMergeCache();

    const first = await resolveFor(101, buildManager('Seeker One'), cache);
    const second = await resolveFor(102, buildManager('Seeker Two'), cache);
    const third = await resolveFor(103, buildManager('Seeker Three'), cache);

    expect(templatesRepo.findByProgramAndAccessKey).toHaveBeenCalledTimes(1);
    expect(mergeInfoRepo.find).toHaveBeenCalledTimes(1);
    // The whole point: one PDF resolve (one DB read, one presign, at most one puppeteer render)
    // for the entire batch, not one per recipient.
    expect(guidelinesPdf.resolveProgramPdfUrl).toHaveBeenCalledTimes(1);

    // Every recipient still receives the value.
    for (const result of [first, second, third]) {
      expect(result?.mergeInfo.guideline_pdf).toBe('https://s3/guidelines.pdf?signed=1');
    }
  });

  it('still resolves per-record fields for every recipient', async () => {
    const cache = createSendMergeCache();

    const first = await resolveFor(101, buildManager('Seeker One'), cache);
    const second = await resolveFor(102, buildManager('Seeker Two'), cache);

    // reg_name is isCommon:false — caching it would send everyone the first recipient's name.
    expect(first?.mergeInfo.reg_name).toBe('Seeker One');
    expect(second?.mergeInfo.reg_name).toBe('Seeker Two');
  });

  it('without a cache, every call resolves everything again (single sends are unaffected)', async () => {
    await resolveFor(101, buildManager('Seeker One'));
    await resolveFor(102, buildManager('Seeker Two'));

    expect(templatesRepo.findByProgramAndAccessKey).toHaveBeenCalledTimes(2);
    expect(mergeInfoRepo.find).toHaveBeenCalledTimes(2);
    expect(guidelinesPdf.resolveProgramPdfUrl).toHaveBeenCalledTimes(2);
  });

  it('never caches a per-recipient common_* override, even though the field is flagged isCommon', async () => {
    // Registration-less send (general link / common invite): resolveAllFieldsAsCommon routes every
    // field through the common path, but reg_name's value comes from THIS recipient's
    // common_user_name — caching it would give the whole batch the first recipient's name.
    mergeInfoRepo.find.mockResolvedValue([{ ...fields[1], isCommon: true }]);
    const cache = createSendMergeCache();

    const first = await service.getTemplateWithMergeInfo(
      5,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      undefined,
      undefined,
      { common_user_name: 'Staff One' },
      { resolveAllFieldsAsCommon: true, cache },
    );
    const second = await service.getTemplateWithMergeInfo(
      5,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      undefined,
      undefined,
      { common_user_name: 'Staff Two' },
      { resolveAllFieldsAsCommon: true, cache },
    );

    expect(first?.mergeInfo.reg_name).toBe('Staff One');
    expect(second?.mergeInfo.reg_name).toBe('Staff Two');
  });

  it('caches a template MISS too, so a misconfigured program is not re-queried per recipient', async () => {
    templatesRepo.findByProgramAndAccessKey.mockResolvedValue(null);
    const cache = createSendMergeCache();

    expect(await resolveFor(101, buildManager('Seeker One'), cache)).toBeNull();
    expect(await resolveFor(102, buildManager('Seeker Two'), cache)).toBeNull();

    expect(templatesRepo.findByProgramAndAccessKey).toHaveBeenCalledTimes(1);
  });
});
