import { BadRequestException } from '@nestjs/common';

import { ContractClearanceService } from './contract-clearance.service';
import type { Contract } from './entities/contract.entity';

/**
 * Pre-declaration transit-assignee handshake. GL Ethiopia asks Djibouti who will
 * handle the shipment in transit; Djibouti answers with a name. The customs
 * declaration stays shut until that name exists, and Djibouti may send a
 * different one later.
 */
describe('ContractClearanceService — transit assignee', () => {
  const contract = (over: Partial<Contract> = {}): Contract =>
    ({
      id: 'ctr-1',
      reference: 'CTR-2026-00042',
      tradeDirection: 'IMPORT',
      customsClearingEnabled: true,
      contractKind: 'ONE_TIME',
      ...over,
    }) as Contract;

  let repo: { currentCycle: jest.Mock; updateCycle: jest.Mock };
  let contractsService: { findById: jest.Mock };
  let notifier: {
    transitAssigneeRequested: jest.Mock;
    transitAssigneeAssigned: jest.Mock;
  };
  let service: ContractClearanceService;

  const cycle = (over: Record<string, unknown> = {}) => ({
    id: 'cyc-1',
    transitAssigneeRequestedAt: null,
    transitAssigneeName: null,
    ...over,
  });

  beforeEach(() => {
    repo = {
      currentCycle: jest.fn().mockResolvedValue(cycle()),
      updateCycle: jest.fn().mockResolvedValue(undefined),
    };
    contractsService = { findById: jest.fn().mockResolvedValue(contract()) };
    notifier = {
      transitAssigneeRequested: jest.fn(),
      transitAssigneeAssigned: jest.fn(),
    };
    service = new ContractClearanceService(
      repo as never,
      contractsService as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      {} as never,
      notifier as never,
    );
  });

  describe('request (GL Ethiopia)', () => {
    it('stamps the ask and pings Djibouti', async () => {
      await service.requestTransitAssignee('ctr-1', '  Reefer, needs a cold-chain officer  ', 'et-1');

      const patch = repo.updateCycle.mock.calls[0][1];
      expect(patch.transitAssigneeRequestedAt).toBeInstanceOf(Date);
      expect(patch.transitAssigneeRequestedByUserId).toBe('et-1');
      expect(patch.transitAssigneeRequestNote).toBe(
        'Reefer, needs a cold-chain officer',
      );
      expect(notifier.transitAssigneeRequested).toHaveBeenCalled();
    });
  });

  describe('assign (GL Djibouti)', () => {
    it('records the officer and tells Ethiopia they can proceed', async () => {
      repo.currentCycle.mockResolvedValue(
        cycle({ transitAssigneeRequestedAt: new Date() }),
      );

      await service.assignTransitAssignee('ctr-1', '  Ahmed Bourhan ', 'dj-1');

      const patch = repo.updateCycle.mock.calls[0][1];
      expect(patch.transitAssigneeName).toBe('Ahmed Bourhan');
      expect(patch.transitAssigneeAssignedByUserId).toBe('dj-1');
      expect(notifier.transitAssigneeAssigned).toHaveBeenCalledWith(
        expect.objectContaining({ id: 'ctr-1' }),
        'Ahmed Bourhan',
        null,
      );
    });

    it('reassigns, carrying the previous name into the notice', async () => {
      repo.currentCycle.mockResolvedValue(
        cycle({
          transitAssigneeRequestedAt: new Date(),
          transitAssigneeName: 'Ahmed Bourhan',
        }),
      );

      await service.assignTransitAssignee('ctr-1', 'Fatouma Ali', 'dj-1');

      expect(notifier.transitAssigneeAssigned).toHaveBeenCalledWith(
        expect.anything(),
        'Fatouma Ali',
        'Ahmed Bourhan',
      );
    });

    it('refuses an empty name', async () => {
      repo.currentCycle.mockResolvedValue(
        cycle({ transitAssigneeRequestedAt: new Date() }),
      );
      await expect(
        service.assignTransitAssignee('ctr-1', '   ', 'dj-1'),
      ).rejects.toBeInstanceOf(BadRequestException);
    });

    it('refuses before Ethiopia has asked', async () => {
      await expect(
        service.assignTransitAssignee('ctr-1', 'Ahmed Bourhan', 'dj-1'),
      ).rejects.toThrow(/not requested/i);
    });
  });

  describe('declaration gate', () => {
    const ensure = (c: Contract) =>
      (
        service as unknown as {
          ensureDeclarationPrerequisites: (id: string, c: Contract) => Promise<void>;
        }
      ).ensureDeclarationPrerequisites('ctr-1', c);

    beforeEach(() => {
      // Documents are approved; only the assignee decides the outcome here.
      (
        service as unknown as { isClearanceFullyApproved: unknown }
      ).isClearanceFullyApproved = jest.fn().mockResolvedValue(true);
    });

    it('tells GL to raise the request when none exists', async () => {
      await expect(ensure(contract())).rejects.toThrow(
        /Request a transit assignee/i,
      );
    });

    it('tells GL to wait when Djibouti has not answered', async () => {
      repo.currentCycle.mockResolvedValue(
        cycle({ transitAssigneeRequestedAt: new Date() }),
      );
      await expect(ensure(contract())).rejects.toThrow(/has not assigned/i);
    });

    it('lets the declaration through once the officer is named', async () => {
      repo.currentCycle.mockResolvedValue(
        cycle({
          transitAssigneeRequestedAt: new Date(),
          transitAssigneeName: 'Ahmed Bourhan',
        }),
      );
      (
        service as unknown as { workflowService: unknown }
      ).workflowService = {
        listMilestones: jest
          .fn()
          .mockResolvedValue([
            { milestoneCode: 'DOCUMENTS_APPROVED', status: 'COMPLETED' },
          ]),
      };

      await expect(ensure(contract())).resolves.toBeUndefined();
    });
  });
});
