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

describe('BookingBatchService — PAID reconcile', () => {
  const scheduleId = 'schedule-1';
  const bookingId = 'booking-1';

  const paidBooking = {
    id: bookingId,
    reference: 'BK-2026-000034',
    trainScheduleId: scheduleId,
    status: 'PAID',
    paymentStatus: 'PAID',
    isGovernment: false,
    cargoTotalWeightVgm: 20,
    bookingContainers: [],
  } as unknown as Booking;

  let service: BookingBatchService;
  let bookingsRepository: {
    findPaidUnlinkedForSchedule: jest.Mock;
    findBatchPool: jest.Mock;
    findBatchPoolByRouteDay: jest.Mock;
    findReservedForSchedule: jest.Mock;
    update: jest.Mock;
  };
  let trainScheduleBookingsRepository: {
    existsForBooking: jest.Mock;
    createMany: jest.Mock;
  };
  let trainSchedulesRepository: {
    findByIdWithFullGraph: jest.Mock;
    findAll: jest.Mock;
  };
  let trainSchedulingService: {
    tryAutoWagonAllocation: jest.Mock;
    getBookableSchedules: jest.Mock;
  };
  let dataSource: {
    getRepository: jest.Mock;
    transaction: jest.Mock;
  };
  let notifier: {
    payNow: jest.Mock;
    secured: jest.Mock;
    expired: jest.Mock;
    unplaced: jest.Mock;
  };

  beforeEach(() => {
    bookingsRepository = {
      findPaidUnlinkedForSchedule: jest.fn().mockResolvedValue([]),
      findBatchPool: jest.fn().mockResolvedValue([]),
      findBatchPoolByRouteDay: jest.fn().mockResolvedValue([]),
      findReservedForSchedule: jest.fn().mockResolvedValue([]),
      update: jest.fn().mockResolvedValue(undefined),
    };
    trainScheduleBookingsRepository = {
      existsForBooking: jest.fn().mockResolvedValue(false),
      createMany: jest.fn().mockResolvedValue(undefined),
    };
    trainSchedulesRepository = {
      findByIdWithFullGraph: jest.fn().mockResolvedValue({
        id: scheduleId,
        maxWagons: 10,
        bookingWindowStatus: 'OPEN',
        trainSet: { locomotive: { maxPullWeightTons: 3500, maxTrainLengthMeters: 760 } },
        scheduleBookings: [],
      }),
      findAll: jest.fn().mockResolvedValue([]),
    };
    trainSchedulingService = {
      tryAutoWagonAllocation: jest.fn().mockResolvedValue({
        assignedBookingIds: [],
        deferred: [],
        issues: [],
        violations: [],
      }),
      getBookableSchedules: jest.fn().mockResolvedValue([]),
    };

    const bookingRepo = {
      findOne: jest.fn().mockResolvedValue(paidBooking),
      update: jest.fn().mockResolvedValue(undefined),
      // WagonType.find() / global-rules find() fall back to defaults when empty.
      find: jest.fn().mockResolvedValue([]),
    };
    dataSource = {
      getRepository: jest.fn().mockReturnValue(bookingRepo),
      transaction: jest.fn(async (fn: (m: unknown) => Promise<void>) => {
        const manager = {
          getRepository: () => bookingRepo,
        };
        await fn(manager);
      }),
    };

    notifier = {
      payNow: jest.fn(),
      secured: jest.fn(),
      expired: jest.fn(),
      unplaced: jest.fn(),
    };

    service = new BookingBatchService(
      dataSource as never,
      bookingsRepository as never,
      trainSchedulesRepository as never,
      trainScheduleBookingsRepository as never,
      notifier as never,
      { addTimeout: jest.fn(), deleteTimeout: jest.fn(), doesExist: jest.fn() } as never,
      trainSchedulingService as never,
    );
  });

  it('reconcilePaidUnlinked links PAID bookings without a schedule row', async () => {
    bookingsRepository.findPaidUnlinkedForSchedule.mockResolvedValue([paidBooking]);

    await service.reconcilePaidUnlinked(scheduleId);

    expect(bookingsRepository.findPaidUnlinkedForSchedule).toHaveBeenCalledWith(scheduleId);
    expect(trainScheduleBookingsRepository.createMany).toHaveBeenCalledWith(
      [{ trainScheduleId: scheduleId, bookingId }],
      expect.anything(),
    );
  });

  it('ensurePaidBookingAllocated links PAID booking when not yet linked', async () => {
    await service.ensurePaidBookingAllocated(bookingId);

    expect(trainScheduleBookingsRepository.createMany).toHaveBeenCalledTimes(1);
    expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledWith(scheduleId);
  });

  it('ensurePaidBookingAllocated is idempotent when already linked', async () => {
    trainScheduleBookingsRepository.existsForBooking.mockResolvedValue(true);

    await service.ensurePaidBookingAllocated(bookingId);
    await service.ensurePaidBookingAllocated(bookingId);

    expect(trainScheduleBookingsRepository.createMany).not.toHaveBeenCalled();
    expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledTimes(2);
  });

  it('processSchedule reconciles PAID-unlinked before wagon allocation', async () => {
    const fillSpy = jest.spyOn(service, 'fillSchedule').mockResolvedValue(undefined);
    const settleSpy = jest.spyOn(service, 'settleDueReservations').mockResolvedValue(undefined);
    const reconcileSpy = jest.spyOn(service, 'reconcilePaidUnlinked').mockResolvedValue(undefined);

    await service.processSchedule(scheduleId);

    expect(fillSpy).toHaveBeenCalledWith(scheduleId);
    expect(settleSpy).toHaveBeenCalledWith(scheduleId);
    expect(reconcileSpy).toHaveBeenCalledWith(scheduleId);
    expect(trainSchedulingService.tryAutoWagonAllocation).toHaveBeenCalledWith(scheduleId);

    const fillOrder = fillSpy.mock.invocationCallOrder[0];
    const reconcileOrder = reconcileSpy.mock.invocationCallOrder[0];
    const wagonOrder = trainSchedulingService.tryAutoWagonAllocation.mock.invocationCallOrder[0];
    expect(fillOrder).toBeLessThan(reconcileOrder);
    expect(reconcileOrder).toBeLessThan(wagonOrder);
  });

  describe('fillRouteDay — day-level distribution', () => {
    const originYardId = 'yard-origin';
    const destinationYardId = 'yard-dest';
    const day = '2026-06-20';
    // 06:00Z and 09:00Z on 2026-06-20 both land on the same EAT day.
    const trainA = 'train-a';
    const trainB = 'train-b';

    // A tiny locomotive: default wagon = 14m / 70t → exactly 1 wagon slot fits.
    const smallLoco = { maxPullWeightTons: 70, maxTrainLengthMeters: 14 };

    const commercial = (id: string, priority: number): Booking =>
      ({
        id,
        reference: id,
        isGovernment: false,
        priorityScore: priority,
        status: 'FULLY_EXECUTED',
        wagonsRequired: 1,
        cargoTotalWeightVgm: 10,
        freightType: 'CONTAINER',
        bookingContainers: [],
      }) as unknown as Booking;

    beforeEach(() => {
      // Two OPEN trains on the same route + day, train A earlier than train B.
      trainSchedulingService.getBookableSchedules.mockResolvedValue([
        {
          id: trainA,
          scheduleDate: '2026-06-20T06:00:00.000Z',
          bookingWindowStatus: 'OPEN',
        },
        {
          id: trainB,
          scheduleDate: '2026-06-20T09:00:00.000Z',
          bookingWindowStatus: 'OPEN',
        },
      ]);
      trainSchedulesRepository.findByIdWithFullGraph.mockImplementation((id: string) =>
        Promise.resolve({
          id,
          maxWagons: 1,
          bookingWindowStatus: 'OPEN',
          trainSetId: `set-${id}`,
          trainSet: { locomotive: smallLoco },
          scheduleBookings: [],
        }),
      );
    });

    it('spills overflow to the next train by priority, then reports unplaced', async () => {
      // 3 commercial bookings, descending priority; only 1 fits per train (2 total).
      bookingsRepository.findBatchPoolByRouteDay.mockResolvedValue([
        commercial('hi', 30),
        commercial('mid', 20),
        commercial('lo', 10),
      ]);

      const touched = await service.fillRouteDay(originYardId, destinationYardId, day);

      expect(bookingsRepository.findBatchPoolByRouteDay).toHaveBeenCalledWith(
        originYardId,
        destinationYardId,
        day,
      );
      // Both trains were processed.
      expect(touched).toEqual([trainA, trainB]);
      // Highest priority reserved on train A, next on train B (commercial → reserve).
      const reservedOn = notifier.payNow.mock.calls.map((c) => (c[0] as Booking).id);
      expect(reservedOn).toEqual(['hi', 'mid']);
      // The third booking fits no train and is reported unplaced (and only it).
      expect(notifier.unplaced).toHaveBeenCalledTimes(1);
      expect((notifier.unplaced.mock.calls[0][0] as Booking).id).toBe('lo');
      expect(notifier.unplaced.mock.calls[0][1]).toBe(day);
    });

    it('reserves the chosen train id on each commercial booking', async () => {
      bookingsRepository.findBatchPoolByRouteDay.mockResolvedValue([commercial('hi', 30)]);

      await service.fillRouteDay(originYardId, destinationYardId, day);

      // reserve() persists trainScheduleId so the settle lifecycle can find the train.
      expect(bookingsRepository.update).toHaveBeenCalledWith(
        'hi',
        expect.objectContaining({
          trainScheduleId: trainA,
          status: 'SELECTED_FOR_BATCH',
        }),
      );
    });
  });
});
