import { Test, TestingModule } from '@nestjs/testing';
import { CommunicationTemplatesMasterService } from './communication-templates-master.service';
import { CommunicationTemplatesMasterRepository } from '../repositories/communication-templates-master.repository';
import { CommunicationTemplatesRepository } from '../repositories/communication-templates.repository';
import { MergeInfoAnswerLocationMapRepository } from '../repositories/merge-info-answer-location-map.repository';

describe('CommunicationTemplatesMasterService', () => {
  let service: CommunicationTemplatesMasterService;
  let masterRepo: {
    findAllByProgramTypeId: jest.Mock;
    findActiveSummariesByProgramTypeId: jest.Mock;
    findInvalidMasterIdsForProgramType: jest.Mock;
  };
  let templatesRepo: { createEntity: jest.Mock; applyPerStepSelectionForProgram: jest.Mock };
  let mergeInfoRepo: { createEntity: jest.Mock };

  const master = (id: number, isDefault: boolean) => ({
    id,
    templateId: `tpl-${id}`,
    templateName: `Template ${id}`,
    templateKey: `KEY_${id}`,
    templateAccessKey: `ACCESS_${id}`,
    templateType: 'email',
    category: null,
    sandboxTemplateId: null,
    isDefault,
    step: 'BLESSED',
    mergeFieldMap: [],
  });

  beforeEach(async () => {
    masterRepo = {
      findAllByProgramTypeId: jest.fn(),
      findActiveSummariesByProgramTypeId: jest.fn(),
      findInvalidMasterIdsForProgramType: jest.fn(),
    };
    templatesRepo = {
      createEntity: jest.fn().mockImplementation((data) => Promise.resolve({ id: 1, ...data })),
      applyPerStepSelectionForProgram: jest.fn(),
    };
    mergeInfoRepo = { createEntity: jest.fn() };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CommunicationTemplatesMasterService,
        { provide: CommunicationTemplatesMasterRepository, useValue: masterRepo },
        { provide: CommunicationTemplatesRepository, useValue: templatesRepo },
        { provide: MergeInfoAnswerLocationMapRepository, useValue: mergeInfoRepo },
      ],
    }).compile();

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

  describe('cloneMultipleMasterTemplatesToProgramByProgramType', () => {
    it('computes attached_steps from the per-step selection when one is given', async () => {
      masterRepo.findAllByProgramTypeId.mockResolvedValue([
        master(17, false),
        master(18, true),
        master(19, false),
      ]);

      // Admin attached 17 to BLESSED only, 19 cross-attached to BLESSED + INVOICE,
      // and left 18 off everywhere.
      await service.cloneMultipleMasterTemplatesToProgramByProgramType(
        5,
        100,
        undefined,
        1,
        [
          { step: 'BLESSED', enabledMasterTemplateIds: [17, 19] },
          { step: 'INVOICE', enabledMasterTemplateIds: [19] },
        ],
      );

      const calls = templatesRepo.createEntity.mock.calls.map((c) => c[0]);
      const c17 = calls.find((c) => c.masterTemplateId === 17);
      const c18 = calls.find((c) => c.masterTemplateId === 18);
      const c19 = calls.find((c) => c.masterTemplateId === 19);
      expect(c17.attachedSteps).toEqual(['BLESSED']);
      expect(c17.isEnabled).toBe(true);
      expect(c18.attachedSteps).toEqual([]);
      expect(c18.isEnabled).toBe(false);
      expect(c19.attachedSteps.sort()).toEqual(['BLESSED', 'INVOICE']);
      expect(c19.isEnabled).toBe(true);
    });

    it('seeds attachedSteps to the master native step when no selection is given', async () => {
      masterRepo.findAllByProgramTypeId.mockResolvedValue([master(17, true)]);

      await service.cloneMultipleMasterTemplatesToProgramByProgramType(5, 100, undefined, 1);

      const call = templatesRepo.createEntity.mock.calls[0][0];
      expect(call.attachedSteps).toEqual(['BLESSED']);
    });

    it('treats an empty per-step selection as "disable everywhere"', async () => {
      masterRepo.findAllByProgramTypeId.mockResolvedValue([master(17, true)]);

      await service.cloneMultipleMasterTemplatesToProgramByProgramType(5, 100, undefined, 1, []);

      const call = templatesRepo.createEntity.mock.calls[0][0];
      expect(call.isEnabled).toBe(false);
      expect(call.attachedSteps).toEqual([]);
    });

    it('falls back to is_enabled = master.is_default when no explicit set is given', async () => {
      masterRepo.findAllByProgramTypeId.mockResolvedValue([master(17, true), master(18, false)]);

      await service.cloneMultipleMasterTemplatesToProgramByProgramType(5, 100, undefined, 1);

      const calls = templatesRepo.createEntity.mock.calls.map((c) => c[0]);
      expect(calls.find((c) => c.masterTemplateId === 17).isEnabled).toBe(true);
      expect(calls.find((c) => c.masterTemplateId === 18).isEnabled).toBe(false);
    });

    it('populates master_template_id and snapshot columns on every clone', async () => {
      masterRepo.findAllByProgramTypeId.mockResolvedValue([master(17, true)]);

      await service.cloneMultipleMasterTemplatesToProgramByProgramType(5, 100, undefined, 1);

      expect(templatesRepo.createEntity).toHaveBeenCalledWith(
        expect.objectContaining({
          masterTemplateId: 17,
          step: 'BLESSED',
          programId: 100,
        }),
      );
    });
  });

  describe('validateMasterTemplateIdsForProgramType', () => {
    it('throws when any id is invalid, with offending ids in details', async () => {
      masterRepo.findInvalidMasterIdsForProgramType.mockResolvedValue([99]);

      await expect(
        service.validateMasterTemplateIdsForProgramType(5, [17, 99]),
      ).rejects.toMatchObject({ details: { invalidIds: [99] } });
    });

    it('passes when all ids are valid', async () => {
      masterRepo.findInvalidMasterIdsForProgramType.mockResolvedValue([]);

      await expect(
        service.validateMasterTemplateIdsForProgramType(5, [17, 18]),
      ).resolves.toBeUndefined();
    });
  });

  describe('applyPerStepSelectionForProgram', () => {
    it('delegates to the templates repository', async () => {
      const perStep = [{ step: 'BLESSED', enabledMasterTemplateIds: [17, 19] }];
      await service.applyPerStepSelectionForProgram(100, perStep, 1);
      expect(templatesRepo.applyPerStepSelectionForProgram).toHaveBeenCalledWith(100, perStep, 1);
    });
  });

  describe('validatePerStepSelectionForProgramType', () => {
    beforeEach(() => {
      // Universe for the type: master 17 (BLESSED), 50 (INVOICE).
      masterRepo.findActiveSummariesByProgramTypeId.mockResolvedValue([
        { id: 17, step: 'BLESSED' },
        { id: 50, step: 'INVOICE' },
      ]);
    });

    it('passes for valid steps and ids (including cross-step attachment)', async () => {
      await expect(
        service.validatePerStepSelectionForProgramType(1, [
          { step: 'BLESSED', enabledMasterTemplateIds: [17, 50] }, // 50 attached cross-step — valid id
        ]),
      ).resolves.toBeUndefined();
    });

    it('rejects an unknown step', async () => {
      await expect(
        service.validatePerStepSelectionForProgramType(1, [
          { step: 'NOT_A_STEP', enabledMasterTemplateIds: [17] },
        ]),
      ).rejects.toMatchObject({ details: { invalidSteps: ['NOT_A_STEP'], invalidIds: [] } });
    });

    it('rejects ids that are not masters of the type', async () => {
      await expect(
        service.validatePerStepSelectionForProgramType(1, [
          { step: 'BLESSED', enabledMasterTemplateIds: [17, 999] },
        ]),
      ).rejects.toMatchObject({ details: { invalidIds: [999] } });
    });
  });
});
