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

import { BookingClearanceService } from './booking-clearance.service';
import type { Booking } from '../bookings/entities/booking.entity';

const generalImportBooking = {
  id: 'b-general',
  status: 'DOCUMENTS_UNDER_REVIEW',
  tradeDirection: 'IMPORT',
  freightType: 'CONTAINER',
  customsClearingEnabled: true,
  contractKind: 'GENERAL',
  contractId: 'c-1',
  dutyRequired: true,
  roHoldReason: null,
  vesselDepartureDate: null,
} as Booking;

const generalExportBooking = {
  ...generalImportBooking,
  id: 'b-export',
  tradeDirection: 'EXPORT',
  dutyRequired: null,
} as Booking;

function makeService(overrides?: {
  booking?: Booking;
  workflowThrows?: boolean;
}) {
  const booking = overrides?.booking ?? generalImportBooking;
  const bookingsRepository = {
    findDocumentReviews: jest.fn().mockResolvedValue([]),
    update: jest.fn().mockResolvedValue(booking),
  };
  const bookingsService = {
    findById: jest.fn().mockResolvedValue(booking),
  };
  const filesService = {
    upsertByCode: jest.fn().mockResolvedValue({}),
    findByResource: jest.fn().mockResolvedValue([]),
  };
  const fileUploadSettingsService = {
    getByCode: jest.fn().mockRejectedValue(new Error('no setting')),
  };
  const workflowService = {
    assertPriorCompleteForBooking: overrides?.workflowThrows
      ? jest.fn().mockRejectedValue(new BadRequestException('Prior milestone incomplete'))
      : jest.fn().mockResolvedValue(undefined),
    completeMilestoneForBooking: jest.fn().mockResolvedValue(undefined),
    onDeclarationUploadedForBooking: jest.fn().mockResolvedValue(undefined),
    onDutySkippedForBooking: jest.fn().mockResolvedValue(undefined),
    listMilestonesForBooking: jest.fn().mockResolvedValue([]),
    resolvePhaseForBooking: jest.fn().mockReturnValue(null),
    computeNextActionForBooking: jest.fn().mockReturnValue(null),
    isBoundaryCompleteForBooking: jest.fn().mockResolvedValue(false),
    markReadyForOperation: jest.fn().mockResolvedValue(undefined),
    onExportReleasedForBooking: jest.fn().mockResolvedValue(undefined),
  };
  const milestoneService = {
    adviseDuty: jest.fn().mockResolvedValue(undefined),
  };
  const dropdownSettingsService = {
    getByCode: jest.fn().mockResolvedValue({
      children: [{ value: '2' }],
    }),
  };
  const glOperationsService = {
    t1State: jest.fn().mockResolvedValue({
      bookingId: 'b-general',
      wagonAllocated: false,
      trainDepartedAt: null,
      trainArrivedAt: null,
      closed: false,
      closedAt: null,
    }),
  };

  const service = new BookingClearanceService(
    bookingsRepository as never,
    bookingsService as never,
    filesService as never,
    fileUploadSettingsService as never,
    workflowService as never,
    milestoneService as never,
    dropdownSettingsService as never,
    glOperationsService as never,
    {
      dutyAdvised: jest.fn(),
      clearanceReady: jest.fn(),
      documentQueried: jest.fn(),
      dutySlipUploadedToStaff: jest.fn(),
      clearanceDocsUploadedToStaff: jest.fn(),
    } as never, // notifier
  );

  return {
    service,
    bookingsRepository,
    bookingsService,
    filesService,
    workflowService,
    milestoneService,
  };
}

describe('BookingClearanceService', () => {
  describe('adviseDuty', () => {
    it('skips duty milestones when duty is not required', async () => {
      const { service, workflowService, bookingsRepository } = makeService();
      await service.adviseDuty('b-general', { dutyRequired: false });

      expect(workflowService.onDutySkippedForBooking).toHaveBeenCalledWith('b-general');
      expect(bookingsRepository.update).toHaveBeenCalledWith(
        'b-general',
        expect.objectContaining({
          dutyRequired: false,
          clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
        }),
      );
    });

    it('records duty advice when duty applies', async () => {
      const { service, milestoneService } = makeService();
      await service.adviseDuty(
        'b-general',
        {
          dutyRequired: true,
          amount: 1500,
          currency: 'ETB',
          declarationSerial: 'DS-1',
        },
        undefined,
        // The duty notice attachment is now mandatory when duty applies.
        { fieldname: 'duty_tax_notice' } as Express.Multer.File,
      );

      expect(milestoneService.adviseDuty).toHaveBeenCalledWith(
        'b-general',
        expect.objectContaining({ amount: 1500, currency: 'ETB', declarationSerial: 'DS-1' }),
        undefined,
      );
    });
  });

  describe('uploadDutySlip', () => {
    it('rejects when duty is not required', async () => {
      const { service } = makeService({
        booking: { ...generalImportBooking, dutyRequired: false } as Booking,
      });
      await expect(
        service.uploadDutySlip('b-general', { fieldname: 'file' } as Express.Multer.File),
      ).rejects.toBeInstanceOf(BadRequestException);
    });

    it('uploads slip and completes DUTY_TAX_PAID on happy path', async () => {
      const { service, filesService, workflowService, bookingsRepository } = makeService();
      const file = { fieldname: 'file' } as Express.Multer.File;

      await service.uploadDutySlip('b-general', file);

      expect(filesService.upsertByCode).toHaveBeenCalledWith(
        expect.objectContaining({
          resourceId: 'b-general',
          code: 'duty_tax_receipt',
          file,
        }),
      );
      expect(workflowService.completeMilestoneForBooking).toHaveBeenCalledWith(
        'b-general',
        'DUTY_TAX_PAID',
      );
      expect(bookingsRepository.update).toHaveBeenCalledWith(
        'b-general',
        expect.objectContaining({
          clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance,
        }),
      );
    });
  });

  describe('uploadDeclaration', () => {
    it('rejects when a prior milestone is incomplete', async () => {
      const { service } = makeService({ workflowThrows: true });
      await expect(
        service.uploadDeclaration('b-general', [{ fieldname: 'decl' } as Express.Multer.File]),
      ).rejects.toBeInstanceOf(BadRequestException);
    });
  });

  describe('uploadReleaseOrder', () => {
    it('places RO on hold when vessel departs too soon', async () => {
      const tomorrow = new Date();
      tomorrow.setDate(tomorrow.getDate() + 1);
      const dateStr = tomorrow.toISOString().slice(0, 10);

      const { service, bookingsRepository } = makeService({ booking: generalExportBooking });
      const result = await service.uploadReleaseOrder(
        'b-export',
        { fieldname: 'ro' } as Express.Multer.File,
        dateStr,
      );

      expect(result.hold).toBe(true);
      expect(result.holdReason).toMatch(/minimum lead time/i);
      expect(bookingsRepository.update).toHaveBeenCalledWith(
        'b-export',
        expect.objectContaining({ roHoldReason: expect.any(String) }),
      );
    });
  });
});
