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

import { BookingBatchService } from './booking-batch.service';
import { Booking } from '../bookings/entities/booking.entity';

/**
 * Export whole-booking single-train gate: an export booking never splits — it
 * rides one train whole or is rejected. The report must try every fillable
 * train on the day (first full → use the second), and when none fits, say how
 * much space is still bookable so the customer knows what he CAN book.
 */
describe('BookingBatchService — exportSpaceReport (whole-booking, single train)', () => {
  const DAY = new Date('2026-07-20T10:00:00Z');

  const schedule = (id: string) => ({
    id,
    status: 'SCHEDULED',
    direction: 'EXPORT',
    scheduledDepartureDate: DAY,
    bookingWindowStatus: 'OPEN',
    windowPhase: null, // legacy gate: OPEN alone makes it fillable
  });

  const fullGraph = (id: string) => ({
    id,
    originStationId: 'yard-a',
    destinationStationId: 'yard-b',
    routeId: null, // legacy two-stop pseudo-route — no milestone query
    scheduleBookings: [],
    trainSet: {
      locomotive: {
        maxPullWeightTons: 500,
        maxTrainLengthMeters: 140,
        overageToleranceTons: 0,
        overageToleranceMeters: 0,
      },
    },
  });

  const exportBooking = (cargoTons: number) =>
    ({
      id: 'bk-exp',
      freightType: 'BULK',
      tradeDirection: 'EXPORT',
      scheduledDate: DAY,
      originYardId: 'yard-a',
      destinationYardId: 'yard-b',
      cargoTotalWeightVgm: cargoTons,
      bookingContainers: [],
    }) as unknown as Booking;

  // A reserved bulk booking heavy enough to exhaust the 500t pull budget.
  const heavyReserved = {
    id: 'bk-heavy',
    freightType: 'BULK',
    originYardId: 'yard-a',
    destinationYardId: 'yard-b',
    cargoTotalWeightVgm: 476,
    bookingContainers: [],
  } as unknown as Booking;

  let service: BookingBatchService;
  let trainSchedulesRepository: {
    findAll: jest.Mock;
    findByIdWithFullGraph: jest.Mock;
    findById: jest.Mock;
  };
  let bookingsRepository: { findReservedForSchedule: jest.Mock };

  beforeEach(() => {
    trainSchedulesRepository = {
      findAll: jest.fn().mockResolvedValue([]),
      findByIdWithFullGraph: jest
        .fn()
        .mockImplementation(async (id: string) => fullGraph(id)),
      findById: jest.fn(),
    };
    bookingsRepository = { findReservedForSchedule: jest.fn().mockResolvedValue([]) };

    // WagonType.find() → [] so representative default dims apply (bulk 60t
    // payload / 23.4t tare / 14m); RouteMilestone is never queried (routeId null).
    const genericRepo = { find: jest.fn().mockResolvedValue([]) };
    const dataSource = { getRepository: jest.fn().mockReturnValue(genericRepo) };

    service = new BookingBatchService(
      dataSource as never,
      bookingsRepository as never,
      trainSchedulesRepository as never,
      {} as never, // trainScheduleBookingsRepository
      {} as never, // notifier
      {} as never, // scheduler
      {} as never, // trainSchedulingService
      {} as never, // billing
      {} as never, // bookingWindowGateway
      {} as never, // pricingService
    );
  });

  it('uses the other train when the first one is full', async () => {
    trainSchedulesRepository.findAll.mockResolvedValue([
      schedule('train-1'),
      schedule('train-2'),
    ]);
    bookingsRepository.findReservedForSchedule.mockImplementation(
      async (id: string) => (id === 'train-1' ? [heavyReserved] : []),
    );

    const report = await service.exportSpaceReport(exportBooking(60));

    expect(report.scheduleId).toBe('train-2');
  });

  it('rejects a booking no single train fits and reports the bookable space', async () => {
    trainSchedulesRepository.findAll.mockResolvedValue([schedule('train-1')]);

    const report = await service.exportSpaceReport(exportBooking(900));

    expect(report.scheduleId).toBeNull();
    expect(report.bestAvailable).not.toBeNull();
    expect(report.bestAvailable!.cargoTons).toBeGreaterThan(0);
    expect(report.bestAvailable!.cargoTons).toBeLessThan(900);
    expect(report.fullMessage).toMatch(/largest remaining space is about .* tons/);
    expect(report.fullMessage).toMatch(/single train whole/);

    await expect(service.pickExportSchedule(exportBooking(900))).rejects.toThrow(
      ConflictException,
    );
  });

  it('says no train is accepting bookings when the day has none', async () => {
    trainSchedulesRepository.findAll.mockResolvedValue([]);

    const report = await service.exportSpaceReport(exportBooking(60));

    expect(report.scheduleId).toBeNull();
    expect(report.fullMessage).toBe(
      'No export train is accepting bookings for this day',
    );
  });

  describe('dayImportAvailability (advisory, summed across the day)', () => {
    const DAY_STR = '2026-07-20';

    const importSchedule = (id: string, over: Record<string, unknown> = {}) => ({
      id,
      status: 'SCHEDULED',
      direction: 'IMPORT',
      scheduledDepartureDate: DAY,
      bookingWindowStatus: 'OPEN',
      windowPhase: 'OPEN', // still OPEN — the advisory ignores the fill phase
      ...over,
    });

    // A bulk booking small enough to fit; freeWagons is what matters, not `fits`.
    const importBooking = (cargoTons: number) =>
      ({
        id: 'bk-imp',
        freightType: 'BULK',
        tradeDirection: 'IMPORT',
        originYardId: 'yard-a',
        destinationYardId: 'yard-b',
        cargoTotalWeightVgm: cargoTons,
        bookingContainers: [],
      }) as unknown as Booking;

    it('sums free wagons across every import train on the day', async () => {
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1'),
        importSchedule('train-2'),
      ]);

      const one = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );
      // Re-run with a single train to prove two trains sum to double one train.
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1'),
      ]);
      const solo = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );

      expect(solo.freeWagons).toBeGreaterThan(0);
      expect(one.freeWagons).toBe(solo.freeWagons * 2);
      expect(one.trainsForDay).toBe(true);
    });

    it('ignores EXPORT trains — they are not part of the import pool', async () => {
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1'),
        { ...importSchedule('train-2'), direction: 'EXPORT' },
      ]);

      const both = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1'),
      ]);
      const solo = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );

      expect(both.freeWagons).toBe(solo.freeWagons);
    });

    it('ignores FULL trains', async () => {
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1', { bookingWindowStatus: 'FULL' }),
      ]);

      const report = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );

      expect(report.freeWagons).toBe(0);
      expect(report.trainsForDay).toBe(false);
    });

    it('nets out capacity already held by reserved bookings', async () => {
      trainSchedulesRepository.findAll.mockResolvedValue([
        importSchedule('train-1'),
      ]);
      const empty = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );

      bookingsRepository.findReservedForSchedule.mockResolvedValue([
        heavyReserved,
      ]);
      const withHold = await service.dayImportAvailability(
        importBooking(60),
        DAY_STR,
      );

      expect(withHold.freeWagons).toBeLessThan(empty.freeWagons);
    });
  });
});
