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

/**
 * Focused tests for the clearance 100%-approved gate in finalizeClearance.
 * Uses minimal stubs for the service's collaborators.
 */
describe('BookingTransitionService — finalizeClearance gate', () => {
  const booking = {
    id: 'b-1',
    status: 'DOCUMENTS_UNDER_REVIEW',
    tradeDirection: 'IMPORT',
    freightType: 'CONTAINER',
    serviceType: { includesCustoms: false }, // no output set → only the input gate
  };

  // Input set has two required docs. Non-customs bookings resolve to the
  // ONE_TIME self-clearance document set.
  const inputSetting = {
    code: 'contract_clearance_selfclear_import_container',
    fields: [
      { fileKey: 'commercial_invoice', isRequired: true },
      { fileKey: 'packing_list', isRequired: true },
    ],
  };

  function makeService(reviews: Array<{ settingCode: string; fileKey: string; status: string }>) {
    const bookingsRepository = {
      findDocumentReviews: jest.fn().mockResolvedValue(reviews),
      update: jest.fn().mockResolvedValue({ id: 'b-1' }),
    };
    const bookingsService = {
      findById: jest.fn().mockResolvedValue(booking),
    };
    const fileUploadSettingsService = {
      getByCode: jest.fn().mockResolvedValue(inputSetting),
    };
    const filesService = { findByResource: jest.fn().mockResolvedValue([]) };

    const service = new BookingTransitionService(
      bookingsRepository as never,
      {} as never, // ruleEngineService
      {} as never, // pricingService
      {} as never, // contractService
      filesService as never,
      fileUploadSettingsService as never,
      {} as never, // bookingBatchService
      bookingsService as never,
      { isPhasedGeneralCustomsBooking: () => false } as never,
      {} as never, // workflowService
      {} as never, // invoiceService
      { 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 };
  }

  it('rejects when a required document is not APPROVED', async () => {
    const { service } = makeService([
      {
        settingCode: inputSetting.code,
        fileKey: 'commercial_invoice',
        status: 'APPROVED',
      },
      // packing_list is still PENDING (missing approval)
    ]);
    await expect(service.finalizeClearance('b-1')).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('moves to CLEARANCE_READY when all required documents are APPROVED (non-customs, no output set)', async () => {
    const { service, bookingsRepository } = makeService([
      { settingCode: inputSetting.code, fileKey: 'commercial_invoice', status: 'APPROVED' },
      { settingCode: inputSetting.code, fileKey: 'packing_list', status: 'APPROVED' },
    ]);
    await service.finalizeClearance('b-1');
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-1',
      expect.objectContaining({ status: 'CLEARANCE_READY' }),
    );
  });
});

/**
 * Customs bookings additionally require the GL output documents before
 * finalizing — they are cleared by Global Logistics, not the customer alone.
 */
describe('BookingTransitionService — finalizeClearance customs output gate', () => {
  const customsBooking = {
    id: 'b-2',
    status: 'DOCUMENTS_UNDER_REVIEW',
    tradeDirection: 'IMPORT',
    freightType: 'CONTAINER',
    serviceType: { includesCustoms: true }, // input + output sets apply
  };

  const inputSetting = {
    code: 'clearance_import_container_with_customs',
    fields: [{ fileKey: 'commercial_invoice', isRequired: true }],
  };
  const outputSetting = {
    code: 'clearance_output_import_container',
    fields: [{ fileKey: 'im4', fileLabel: 'IM4 declaration', isRequired: true }],
  };

  function makeCustomsService(uploadedOutputCodes: string[]) {
    const bookingsRepository = {
      findDocumentReviews: jest.fn().mockResolvedValue([
        { settingCode: inputSetting.code, fileKey: 'commercial_invoice', status: 'APPROVED' },
      ]),
      update: jest.fn().mockResolvedValue({ id: 'b-2' }),
    };
    const bookingsService = { findById: jest.fn().mockResolvedValue(customsBooking) };
    const fileUploadSettingsService = {
      getByCode: jest.fn((code: string) =>
        Promise.resolve(code === outputSetting.code ? outputSetting : inputSetting),
      ),
    };
    const filesService = {
      findByResource: jest
        .fn()
        .mockResolvedValue(uploadedOutputCodes.map((code) => ({ code }))),
    };

    const service = new BookingTransitionService(
      bookingsRepository as never,
      {} as never,
      {} as never,
      {} as never,
      filesService as never,
      fileUploadSettingsService as never,
      {} as never, // bookingBatchService
      bookingsService as never,
      { isPhasedGeneralCustomsBooking: () => false } as never,
      {} as never, // workflowService
      {} as never, // invoiceService
      { 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 };
  }

  it('rejects when required customs output documents are missing', async () => {
    const { service } = makeCustomsService([]); // no output uploaded
    await expect(service.finalizeClearance('b-2')).rejects.toBeInstanceOf(
      BadRequestException,
    );
  });

  it('moves to CLEARANCE_READY when input is approved and output docs are present', async () => {
    const { service, bookingsRepository } = makeCustomsService(['im4']);
    await service.finalizeClearance('b-2');
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-2',
      expect.objectContaining({ status: 'CLEARANCE_READY' }),
    );
  });
});

/**
 * The first clearance submission (AWAITING_DOCUMENTS) must include every
 * required input document; subsequent re-uploads during review only need the
 * specific files being fixed, so already-uploaded required docs stay in place.
 */
describe('BookingTransitionService — submitClearanceDocuments required-fields gate', () => {
  const inputSetting = {
    code: 'contract_clearance_selfclear_import_container',
    fields: [
      { fileKey: 'commercial_invoice', fileLabel: 'Commercial invoice', isRequired: true },
      { fileKey: 'packing_list', fileLabel: 'Packing list', isRequired: true },
    ],
  };

  function makeService(status: string, existingCodes: string[]) {
    const bookingsRepository = {
      upsertDocumentReviewPending: jest.fn().mockResolvedValue(undefined),
      update: jest.fn().mockResolvedValue({ id: 'b-3' }),
    };
    const booking = {
      id: 'b-3',
      status,
      tradeDirection: 'IMPORT',
      freightType: 'CONTAINER',
      serviceType: { includesCustoms: false },
    };
    const bookingsService = { findById: jest.fn().mockResolvedValue(booking) };
    const fileUploadSettingsService = {
      getByCode: jest.fn().mockResolvedValue(inputSetting),
    };
    const filesService = {
      findByResource: jest
        .fn()
        .mockResolvedValue(existingCodes.map((code) => ({ code }))),
      upsertByCode: jest.fn().mockResolvedValue({ id: 'file-rec' }),
    };

    const service = new BookingTransitionService(
      bookingsRepository as never,
      {} as never,
      {} as never,
      {} as never,
      filesService as never,
      fileUploadSettingsService as never,
      {} as never, // bookingBatchService
      bookingsService as never,
      { isPhasedGeneralCustomsBooking: () => false } as never,
      {} as never, // workflowService
      {} as never, // invoiceService
      { 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, filesService };
  }

  function fakeFile(fieldname: string): Express.Multer.File {
    return { fieldname, originalname: `${fieldname}.pdf` } as Express.Multer.File;
  }

  it('rejects the first submission when a required document is missing', async () => {
    const { service } = makeService('AWAITING_DOCUMENTS', []);
    await expect(
      service.submitClearanceDocuments('b-3', [fakeFile('commercial_invoice')]),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('accepts the first submission when every required document is provided', async () => {
    const { service, bookingsRepository } = makeService('AWAITING_DOCUMENTS', []);
    await service.submitClearanceDocuments('b-3', [
      fakeFile('commercial_invoice'),
      fakeFile('packing_list'),
    ]);
    expect(bookingsRepository.update).toHaveBeenCalledWith(
      'b-3',
      expect.objectContaining({ status: 'DOCUMENTS_UNDER_REVIEW' }),
    );
  });

  it('allows re-uploading a single queried document during review without re-sending the rest', async () => {
    // packing_list was already uploaded in the first round; the customer is now
    // only re-uploading the queried commercial_invoice.
    const { service, bookingsRepository } = makeService(
      'DOCUMENTS_UNDER_REVIEW',
      ['packing_list'],
    );
    await service.submitClearanceDocuments('b-3', [
      fakeFile('commercial_invoice'),
    ]);
    // Only the re-uploaded doc is touched — no full re-gate, no rework on the rest.
    expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledTimes(1);
    expect(bookingsRepository.upsertDocumentReviewPending).toHaveBeenCalledWith(
      expect.objectContaining({ fileKey: 'commercial_invoice' }),
    );
  });
});
