import { BadRequestException, ConflictException } from '@nestjs/common';
import { BookingTransitionService } from './booking-transition.service';

/**
 * Operation-request review for general-contract drawdown orders:
 *  - ACCEPT a train order → FULLY_EXECUTED with the invoice ensured; import/
 *    domestic bookings wait for their booking-day window cycle (no immediate
 *    batch enqueue at accept time).
 *  - ACCEPT a road order  → ROAD_DISPATCH_PENDING, never enters the train batch.
 *  - REQUEST_CHANGES requires a note → OPERATION_CHANGES_REQUESTED.
 */
describe('BookingTransitionService — operation review', () => {
  function makeService(serviceTypeCode: string) {
    const booking = {
      id: 'b-1',
      reference: 'BKG-1',
      status: 'OPERATION_REQUEST_PENDING',
      originYardId: 'o-1',
      destinationYardId: 'd-1',
      scheduledDate: new Date('2026-07-01T00:00:00.000Z'),
      serviceType: { code: serviceTypeCode },
    };
    const bookingsRepository = {
      update: jest.fn().mockResolvedValue({ id: 'b-1' }),
      createReviewNote: jest.fn().mockResolvedValue(undefined),
    };
    const bookingsService = {
      findById: jest.fn().mockResolvedValue(booking),
    };
    const bookingBatchService = {
      enqueueRouteDayProcessing: jest.fn(),
      pickExportSchedule: jest.fn(),
      acceptExportBooking: jest.fn(),
    };
    const invoiceService = {
      ensureInvoiceForBooking: jest
        .fn()
        .mockResolvedValue({ id: 'inv-1', invoiceNumber: 'INV-0001' }),
      updateStatus: jest.fn().mockResolvedValue(undefined),
    };

    const service = new BookingTransitionService(
      bookingsRepository as never,
      {} as never, // ruleEngineService
      {} as never, // pricingService
      {} as never, // contractService
      {} as never, // filesService
      {} as never, // fileUploadSettingsService
      bookingBatchService as never,
      bookingsService as never,
      { isPhasedGeneralCustomsBooking: () => false } as never,
      {} as never, // workflowService
      invoiceService as never,
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      {
        accepted: jest.fn(),
        approved: jest.fn(),
        rejected: jest.fn(),
        changesRequested: jest.fn(),
        documentQueried: jest.fn(),
        clearanceReady: jest.fn(),
        operationChangesRequested: jest.fn(),
        operationAccepted: jest.fn(),
        inTransit: jest.fn(),
        completed: jest.fn(),
        cancelled: jest.fn(),
        submittedToStaff: jest.fn(),
        customerSignedToStaff: jest.fn(),
        operationRequestedToStaff: jest.fn(),
        clearanceDocsUploadedToStaff: jest.fn(),
        dutySlipUploadedToStaff: jest.fn(),
      } as never, // notifier
    );
    return { service, bookingsRepository, bookingBatchService, invoiceService };
  }

  it('ACCEPT of a train order → FULLY_EXECUTED, invoice ensured, batch waits for window cycle', async () => {
    const { service, bookingsRepository, bookingBatchService, invoiceService } =
      makeService('RAIL_CONTAINER');
    await service.reviewOperationRequest('b-1', 'ACCEPT', 'staff-1');
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'FULLY_EXECUTED' }),
    );
    expect(invoiceService.ensureInvoiceForBooking).toHaveBeenCalledTimes(1);
    // Import/domestic train bookings are batched by the window cycle later —
    // never enqueued directly at accept time.
    expect(bookingBatchService.enqueueRouteDayProcessing).not.toHaveBeenCalled();
    expect(bookingBatchService.acceptExportBooking).not.toHaveBeenCalled();
  });

  it('ACCEPT of a road order → ROAD_DISPATCH_PENDING and does NOT enter the batch', async () => {
    const { service, bookingsRepository, bookingBatchService, invoiceService } =
      makeService('ROAD_CONTAINER');
    await service.reviewOperationRequest('b-1', 'ACCEPT', 'staff-1');
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'ROAD_DISPATCH_PENDING' }),
    );
    expect(invoiceService.ensureInvoiceForBooking).toHaveBeenCalledTimes(1);
    expect(bookingBatchService.enqueueRouteDayProcessing).not.toHaveBeenCalled();
  });

  it('REQUEST_CHANGES requires a note → OPERATION_CHANGES_REQUESTED', async () => {
    const { service, bookingsRepository } = makeService('RAIL_CONTAINER');
    await expect(
      service.reviewOperationRequest('b-1', 'REQUEST_CHANGES', 'staff-1', {}),
    ).rejects.toBeInstanceOf(BadRequestException);

    await service.reviewOperationRequest('b-1', 'REQUEST_CHANGES', 'staff-1', {
      note: 'Fix the schedule',
    });
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'OPERATION_CHANGES_REQUESTED' }),
    );
  });
});

/**
 * Export over-book gate at the customer's requestOperation step: export never
 * splits, so the free-space check runs the moment the customer commits to a
 * shipment day. When no single export train that day can carry the whole
 * booking, `pickExportSchedule` throws and the request is refused BEFORE the
 * booking moves to OPERATION_REQUEST_PENDING. Import bookings are never gated
 * here (they are batched + splittable later).
 */
describe('BookingTransitionService — requestOperation export space gate', () => {
  function makeService(tradeDirection: 'EXPORT' | 'IMPORT', overbook: boolean) {
    const booking = {
      id: 'b-1',
      reference: 'BKG-1',
      status: 'CLEARANCE_READY',
      tradeDirection,
      originYardId: 'o-1',
      destinationYardId: 'd-1',
      totalAmount: 1000,
      contractId: null,
      serviceType: { code: 'RAIL_CONTAINER' },
    };
    const bookingsRepository = {
      update: jest.fn().mockResolvedValue({ id: 'b-1' }),
    };
    const bookingsService = {
      findById: jest.fn().mockResolvedValue(booking),
      checkDayCompatibilityForBooking: jest
        .fn()
        .mockResolvedValue({ hasDeparture: true, hasCompatible: true }),
    };
    const bookingBatchService = {
      // Over-book → the export gate rejects; otherwise it returns a schedule id.
      pickExportSchedule: overbook
        ? jest.fn().mockRejectedValue(new ConflictException('Not enough train space'))
        : jest.fn().mockResolvedValue('sched-1'),
    };
    const notifier = { operationRequestedToStaff: jest.fn() };

    const service = new BookingTransitionService(
      bookingsRepository as never,
      {} as never, // ruleEngineService
      {} as never, // pricingService
      {} as never, // contractService
      {} as never, // filesService
      {} as never, // fileUploadSettingsService
      bookingBatchService as never,
      bookingsService as never,
      { isPhasedGeneralCustomsBooking: () => false } as never,
      {} as never, // workflowService
      {} as never, // invoiceService
      { validate20ftPairing: jest.fn().mockResolvedValue([]) } as never,
      notifier as never,
    );
    return { service, bookingsRepository, bookingBatchService };
  }

  it('rejects an over-booked export request and does NOT advance the booking', async () => {
    const { service, bookingsRepository, bookingBatchService } = makeService(
      'EXPORT',
      true,
    );
    await expect(
      service.requestOperation('b-1', '2026-07-20T00:00:00.000Z'),
    ).rejects.toBeInstanceOf(ConflictException);
    expect(bookingBatchService.pickExportSchedule).toHaveBeenCalledTimes(1);
    expect(bookingsRepository.update).not.toHaveBeenCalled();
  });

  it('lets an export request through when a train fits the whole booking', async () => {
    const { service, bookingsRepository, bookingBatchService } = makeService(
      'EXPORT',
      false,
    );
    await service.requestOperation('b-1', '2026-07-20T00:00:00.000Z');
    expect(bookingBatchService.pickExportSchedule).toHaveBeenCalledTimes(1);
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'OPERATION_REQUEST_PENDING' }),
    );
  });

  it('never runs the export gate for an import request', async () => {
    const { service, bookingsRepository, bookingBatchService } = makeService(
      'IMPORT',
      true, // would reject IF called — proves it is not called
    );
    await service.requestOperation('b-1', '2026-07-20T00:00:00.000Z');
    expect(bookingBatchService.pickExportSchedule).not.toHaveBeenCalled();
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'OPERATION_REQUEST_PENDING' }),
    );
  });
});
