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 three computed merge fields PT_PAP's emails need, which no other program type had:
 *
 *   program_dates      formatProgramDayDates  every calendar day of the stay, not a "X to Y" range
 *   checkin_time_date  getCheckInWindow       date-first time window prose
 *   checkout_time_date getCheckOutDeadline    date-first time window prose
 *
 * PAP is residential and approval-gated: the seat (and therefore the sub-program whose dates these
 * are) is only allocated at approval, so the pre-approval registration-completion email must still
 * render — falling back to the parent program. Both paths are asserted here.
 *
 * The exact output strings are the contract with the Zepto templates, so they are asserted
 * literally rather than by shape.
 */
describe('CommunicationMergeDataService — PAP program date / check-in / check-out fields', () => {
  let service: CommunicationMergeDataService;
  let mergeInfoRepo: { find: jest.Mock };

  const accessKey = CommunicationTemplateAccessKeyEnum.REGISTRATION_COMPLETED_EMAIL_SEEKER;

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

  const papFields = [
    {
      keyName: 'program_dates',
      sourceTable: 'computed',
      sourceColumn: 'formatProgramDayDates',
      isCommon: false,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
    {
      keyName: 'checkin_time_date',
      sourceTable: 'computed',
      sourceColumn: 'getCheckInWindow',
      isCommon: false,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
    {
      keyName: 'checkout_time_date',
      sourceTable: 'computed',
      sourceColumn: 'getCheckOutDeadline',
      isCommon: false,
      dataType: 'string',
      isNullable: true,
      defaultValue: '',
    },
  ];

  /** Program rows keyed by id, so a test can prove WHICH program was read. */
  type ProgramRow = {
    startsAt?: string;
    endsAt?: string;
    checkinAt?: string;
    checkinEndsAt?: string;
    checkoutAt?: string;
    checkoutEndsAt?: string;
  };

  /**
   * A manager whose getRawOne answers from the table the builder was pointed at and the aliases
   * it selected — enough to serve the registration lookup and both program lookups.
   */
  const buildManager = (
    registration: { allocatedProgramId: number | null; programId: number | null },
    programs: Record<number, ProgramRow>,
  ) => ({
    createQueryBuilder: jest.fn().mockImplementation(() => {
      let table = '';
      const aliases: string[] = [];
      let programId = 0;

      const builder: Record<string, jest.Mock> = {
        select: jest.fn((_col: string, alias?: string) => {
          if (alias) aliases.push(alias);
          return builder;
        }),
        addSelect: jest.fn((_col: string, alias?: string) => {
          if (alias) aliases.push(alias);
          return builder;
        }),
        from: jest.fn((from: string) => {
          table = from;
          return builder;
        }),
        where: jest.fn((_clause: string, params?: Record<string, number>) => {
          if (params?.programId) programId = params.programId;
          return builder;
        }),
        andWhere: jest.fn(() => builder),
        orderBy: jest.fn(() => builder),
        limit: jest.fn(() => builder),
        getSql: jest.fn(() => 'SELECT 1'),
        getRawOne: jest.fn(() => {
          if (table === 'hdb_program_registration') {
            return Promise.resolve(registration);
          }
          const program = programs[programId];
          if (!program) return Promise.resolve(undefined);
          return Promise.resolve(
            aliases.reduce<Record<string, unknown>>((row, alias) => {
              row[alias] = program[alias as keyof ProgramRow] ?? null;
              return row;
            }, {}),
          );
        }),
      };

      return builder;
    }),
  });

  // 28–30 September 2026 IST. Check-in 1:00–3:00 p.m. on the 28th, check-out 11:30 a.m.–1:00 p.m.
  // on the 30th. Stored as UTC, which is what a timestamptz column hands back — IST is UTC+5:30.
  const allocatedSubProgram: ProgramRow = {
    startsAt: '2026-09-28T03:30:00.000Z',
    endsAt: '2026-09-30T12:00:00.000Z',
    checkinAt: '2026-09-28T07:30:00.000Z',
    checkinEndsAt: '2026-09-28T09:30:00.000Z',
    checkoutAt: '2026-09-30T06:00:00.000Z',
    checkoutEndsAt: '2026-09-30T07:30:00.000Z',
  };

  // The grouped parent program spans every sub-program batch: 28 September – 2 October.
  const parentProgram: ProgramRow = {
    startsAt: '2026-09-28T03:30:00.000Z',
    endsAt: '2026-10-02T12:00:00.000Z',
  };

  beforeEach(async () => {
    mergeInfoRepo = { find: jest.fn().mockResolvedValue(papFields) };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CommunicationMergeDataService,
        {
          provide: CommunicationTemplatesRepository,
          useValue: {
            findByProgramAndAccessKey: jest.fn().mockResolvedValue(template),
            getTemplateIdBasedOnEnvironment: jest.fn().mockReturnValue('tpl-1'),
          },
        },
        { 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);
  });

  const resolve = (manager: unknown) =>
    service.getTemplateWithMergeInfo(
      10,
      accessKey,
      CommunicationTypeEnum.EMAIL,
      101,
      manager as never,
    );

  it('renders the allocated sub-program stay as individual days plus its check-in/out prose', async () => {
    const result = await resolve(
      buildManager({ allocatedProgramId: 55, programId: 10 }, { 55: allocatedSubProgram }),
    );

    // Every day of the stay, not "28-09-2026 to 30-09-2026" — a residential program is one
    // continuous stay, and the seeker is told which days they are there.
    expect(result?.mergeInfo.program_dates).toBe('September 28, 29 & 30, 2026');
    expect(result?.mergeInfo.checkin_time_date).toBe(
      'Monday, 28 September 2026, from 1:00 p.m. to 3:00 p.m.',
    );
    expect(result?.mergeInfo.checkout_time_date).toBe(
      'Wednesday, 30 September 2026, from 11:30 a.m. to 1:00 p.m.',
    );
  });

  it('falls back to the parent program before approval, when no seat is allocated yet', async () => {
    // The registration-completion email fires while approvalStatus is still pending, so
    // allocated_program_id is null. It must still render — off the parent program's full span.
    const result = await resolve(
      buildManager({ allocatedProgramId: null, programId: 10 }, { 10: parentProgram }),
    );

    expect(result?.mergeInfo.program_dates).toBe('September 28, 29 & 30, October 1 & 2, 2026');
    // The parent carries no check-in/out times; the fields degrade to empty rather than failing.
    expect(result?.mergeInfo.checkin_time_date).toBe('');
    expect(result?.mergeInfo.checkout_time_date).toBe('');
  });

  it('names both dates when the check-in window crosses midnight', async () => {
    const result = await resolve(
      buildManager(
        { allocatedProgramId: 55, programId: 10 },
        {
          55: {
            ...allocatedSubProgram,
            checkinAt: '2026-09-27T16:30:00.000Z', // 10:00 p.m. IST on the 27th
            checkinEndsAt: '2026-09-27T19:30:00.000Z', // 1:00 a.m. IST on the 28th
          },
        },
      ),
    );

    expect(result?.mergeInfo.checkin_time_date).toBe(
      'Sunday, 27 September 2026, 10:00 p.m. to Monday, 28 September 2026, 1:00 a.m.',
    );
  });

  it('treats a program with no ends_at as a single-day stay', async () => {
    const result = await resolve(
      buildManager(
        { allocatedProgramId: 55, programId: 10 },
        { 55: { ...allocatedSubProgram, endsAt: undefined } },
      ),
    );

    expect(result?.mergeInfo.program_dates).toBe('September 28, 2026');
  });
});
