import { BadRequestException } from '@nestjs/common';
import { ContractDocPhase } from '@edr/types';

import { ClearanceWorkflowService } from './clearance-workflow.service';
import type { ClearanceMilestone } from './entities/clearance-milestone.entity';
import type { Contract } from './entities/contract.entity';
import type { ContractClearanceCycle } from './entities/contract-clearance-cycle.entity';
import type { Booking } from '../bookings/entities/booking.entity';

function ms(
  code: string,
  status: 'PENDING' | 'COMPLETED' | 'SKIPPED',
  ownerRegion: 'ET' | 'DJ' | 'CUST' | 'OPS' = 'ET',
): ClearanceMilestone {
  return { milestoneCode: code, status, ownerRegion } as ClearanceMilestone;
}

function importThroughDeclaration(): ClearanceMilestone[] {
  return [
    ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
    ms('PENDING_DOCUMENT_REVIEW', 'COMPLETED', 'ET'),
    ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
    ms('UNDER_CUSTOMS_CLEARANCE', 'PENDING', 'ET'),
    ms('DECLARED', 'PENDING', 'ET'),
    ms('DUTY_TAXES_ADVISED', 'PENDING', 'ET'),
    ms('DUTY_TAX_PAID', 'PENDING', 'CUST'),
    ms('TRANSIT_PERMIT_UPLOADED', 'PENDING', 'ET'),
    ms('DO_COLLECTED', 'PENDING', 'DJ'),
  ];
}

function makeService(milestones: ClearanceMilestone[]) {
  const contractsRepository = {
    currentCycle: jest.fn(),
    update: jest.fn(),
    setCycleStatus: jest.fn(),
    updateCycle: jest.fn(),
  };
  const milestoneService = {
    listForContract: jest.fn().mockResolvedValue(milestones),
    listForBooking: jest.fn().mockResolvedValue(milestones),
    skipForContract: jest.fn(),
    completeForContract: jest.fn(),
    completeWithMetadataForContract: jest.fn(),
  };
  const bookingsRepository = { update: jest.fn() };
  const service = new ClearanceWorkflowService(
    contractsRepository as never,
    milestoneService as never,
    bookingsRepository as never,
    { clearanceReady: jest.fn() } as never, // notifier
  );
  return { service, milestoneService, contractsRepository, bookingsRepository };
}

const importContract = {
  id: 'c-import',
  tradeDirection: 'IMPORT',
  customsClearingEnabled: true,
  contractKind: 'ONE_TIME',
} as Contract;

const exportContract = {
  id: 'c-export',
  tradeDirection: 'EXPORT',
  customsClearingEnabled: true,
  contractKind: 'ONE_TIME',
} as Contract;

describe('ClearanceWorkflowService', () => {
  describe('boundaryMilestone', () => {
    it('uses DO_COLLECTED for import and EXPORT_RELEASED for export', () => {
      const { service } = makeService([]);
      expect(service.boundaryMilestone('IMPORT')).toBe('DO_COLLECTED');
      expect(service.boundaryMilestone('EXPORT')).toBe('EXPORT_RELEASED');
    });
  });

  describe('assertPriorComplete', () => {
    it('rejects when a prior milestone is still pending', async () => {
      const milestones = importThroughDeclaration().map((m) =>
        m.milestoneCode === 'DOCUMENTS_APPROVED'
          ? ms('DOCUMENTS_APPROVED', 'PENDING', 'ET')
          : m,
      );
      const { service } = makeService(milestones);
      await expect(
        service.assertPriorComplete('c-import', 'IMPORT', 'DECLARED'),
      ).rejects.toBeInstanceOf(BadRequestException);
    });

    it('allows proceeding when prior milestones are completed or skipped', async () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('PENDING_DOCUMENT_REVIEW', 'COMPLETED', 'ET'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('UNDER_CUSTOMS_CLEARANCE', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'SKIPPED', 'ET'),
        ms('DUTY_TAX_PAID', 'SKIPPED', 'CUST'),
        ms('TRANSIT_PERMIT_UPLOADED', 'PENDING', 'ET'),
        ms('DO_COLLECTED', 'PENDING', 'DJ'),
      ];
      const { service } = makeService(milestones);
      await expect(
        service.assertPriorComplete('c-import', 'IMPORT', 'TRANSIT_PERMIT_UPLOADED'),
      ).resolves.toBeUndefined();
    });
  });

  describe('isBoundaryComplete', () => {
    it('returns true only when boundary milestone is completed', async () => {
      const done = [
        ...importThroughDeclaration().slice(0, -1),
        ms('DO_COLLECTED', 'COMPLETED', 'DJ'),
      ];
      const { service: doneSvc } = makeService(done);
      await expect(doneSvc.isBoundaryComplete('c-import', 'IMPORT')).resolves.toBe(true);

      const pending = importThroughDeclaration();
      const { service: pendingSvc } = makeService(pending);
      await expect(pendingSvc.isBoundaryComplete('c-import', 'IMPORT')).resolves.toBe(false);
    });
  });

  describe('onDutySkipped', () => {
    it('skips duty milestones on the contract', async () => {
      const { service, milestoneService } = makeService([]);
      await service.onDutySkipped('c-import');
      expect(milestoneService.skipForContract).toHaveBeenCalledWith(
        'c-import',
        'DUTY_TAXES_ADVISED',
      );
      expect(milestoneService.skipForContract).toHaveBeenCalledWith(
        'c-import',
        'DUTY_TAX_PAID',
      );
    });
  });

  describe('computeNextAction — import happy path', () => {
    it('prompts customer to upload docs first', () => {
      const { service } = makeService([ms('IMPORT_DOCS_UPLOADED', 'PENDING', 'CUST')]);
      const next = service.computeNextAction(importContract, null, [
        ms('IMPORT_DOCS_UPLOADED', 'PENDING', 'CUST'),
      ]);
      expect(next?.actor).toBe('CUSTOMER');
      expect(next?.milestoneCode).toBe('IMPORT_DOCS_UPLOADED');
    });

    it('prompts ET review after customer docs', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'PENDING', 'ET'),
      ];
      const { service } = makeService(milestones);
      const next = service.computeNextAction(importContract, null, milestones);
      expect(next?.actor).toBe('GL_ET');
      expect(next?.action).toMatch(/Review/i);
    });

    it('prompts duty toggle when declaration done and duty unset', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'PENDING', 'ET'),
      ];
      const cycle = { dutyRequired: null } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const next = service.computeNextAction(importContract, cycle, milestones);
      expect(next?.actor).toBe('GL_ET');
      expect(next?.action).toMatch(/duty/i);
    });

    it('prompts customer duty slip when duty required and advised', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'COMPLETED', 'ET'),
        ms('DUTY_TAX_PAID', 'PENDING', 'CUST'),
      ];
      const cycle = { dutyRequired: true } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const next = service.computeNextAction(importContract, cycle, milestones);
      expect(next?.actor).toBe('CUSTOMER');
      expect(next?.milestoneCode).toBe('DUTY_TAX_PAID');
    });

    it('skips duty path when duty not required', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'SKIPPED', 'ET'),
        ms('DUTY_TAX_PAID', 'SKIPPED', 'CUST'),
        ms('TRANSIT_PERMIT_UPLOADED', 'PENDING', 'ET'),
      ];
      const cycle = { dutyRequired: false } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const next = service.computeNextAction(importContract, cycle, milestones);
      expect(next?.actor).toBe('GL_ET');
      expect(next?.milestoneCode).toBe('TRANSIT_PERMIT_UPLOADED');
    });

    it('prompts ET to finalize pre-clearance after transit permit', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'SKIPPED', 'ET'),
        ms('DUTY_TAX_PAID', 'SKIPPED', 'CUST'),
        ms('TRANSIT_PERMIT_UPLOADED', 'COMPLETED', 'ET'),
        ms('DO_COLLECTED', 'PENDING', 'DJ'),
      ];
      const cycle = { dutyRequired: false } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const next = service.computeNextAction(importContract, cycle, milestones);
      expect(next?.actor).toBe('GL_ET');
      expect(next?.action).toMatch(/finalize pre-clearance/i);
    });

    it('prompts DJ for DO then ET booking when pre-booking complete', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'SKIPPED', 'ET'),
        ms('DUTY_TAX_PAID', 'SKIPPED', 'CUST'),
        ms('TRANSIT_PERMIT_UPLOADED', 'COMPLETED', 'ET'),
        ms('DO_COLLECTED', 'PENDING', 'DJ'),
      ];
      const cycle = {
        dutyRequired: false,
        preClearanceFinalizedAt: new Date(),
      } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const djNext = service.computeNextAction(importContract, cycle, milestones);
      expect(djNext?.actor).toBe('GL_DJ');

      const booked = milestones.map((m) =>
        m.milestoneCode === 'DO_COLLECTED' ? ms('DO_COLLECTED', 'COMPLETED', 'DJ') : m,
      );
      const etNext = service.computeNextAction(importContract, cycle, booked);
      expect(etNext?.actor).toBe('GL_ET');
      expect(etNext?.action).toMatch(/booking/i);
    });
  });

  describe('computeNextAction — export RO hold', () => {
    it('surfaces DJ action when RO is on hold', () => {
      const milestones = [
        ms('EXPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('RELEASE_ORDER_SECURED', 'PENDING', 'DJ'),
      ];
      const cycle = {
        roHoldReason: 'Vessel departs in 1 day(s) — minimum lead time is 2 day(s).',
      } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const next = service.computeNextAction(exportContract, cycle, milestones);
      expect(next?.actor).toBe('GL_DJ');
      expect(next?.blockedReason).toMatch(/minimum lead time/i);
    });
  });

  describe('inferPhase', () => {
    it('places import contract in customer duty phase when duty outstanding', () => {
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAX_PAID', 'PENDING', 'CUST'),
      ];
      const cycle = { dutyRequired: true } as ContractClearanceCycle;
      const { service } = makeService(milestones);
      const phase = service.inferPhase(importContract, cycle, milestones);
      expect(phase).toBe(ContractDocPhase.CustomerDuty);
    });
  });

  describe('queue helpers', () => {
    it('returns first pending ET-owned milestone code', () => {
      const { service } = makeService([
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'PENDING', 'ET'),
        ms('DO_COLLECTED', 'PENDING', 'DJ'),
      ]);
      expect(service.etPendingMilestoneCodes([])).toBeNull();
      expect(
        service.etPendingMilestoneCodes([
          ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
          ms('DOCUMENTS_APPROVED', 'PENDING', 'ET'),
        ]),
      ).toBe('DOCUMENTS_APPROVED');
    });

    it('returns first pending DJ-owned milestone code', () => {
      const { service } = makeService([]);
      expect(
        service.djPendingMilestoneCodes([
          ms('TRANSIT_PERMIT_UPLOADED', 'COMPLETED', 'ET'),
          ms('DO_COLLECTED', 'PENDING', 'DJ'),
        ]),
      ).toBe('DO_COLLECTED');
    });
  });

  describe('computeNextActionForBooking', () => {
    it('prompts customer to proceed after import boundary on booking', () => {
      const booking = {
        tradeDirection: 'IMPORT',
        dutyRequired: false,
        preClearanceFinalizedAt: new Date(),
      } as Booking;
      const milestones = [
        ms('IMPORT_DOCS_UPLOADED', 'COMPLETED', 'CUST'),
        ms('DOCUMENTS_APPROVED', 'COMPLETED', 'ET'),
        ms('DECLARED', 'COMPLETED', 'ET'),
        ms('DUTY_TAXES_ADVISED', 'SKIPPED', 'ET'),
        ms('DUTY_TAX_PAID', 'SKIPPED', 'CUST'),
        ms('TRANSIT_PERMIT_UPLOADED', 'COMPLETED', 'ET'),
        ms('DO_COLLECTED', 'COMPLETED', 'DJ'),
      ];
      const { service } = makeService(milestones);
      const next = service.computeNextActionForBooking(booking, milestones);
      expect(next?.actor).toBe('CUSTOMER');
      expect(next?.action).toMatch(/operation/i);
    });
  });
});
