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

import { compareSchedulingPriority } from '../scheduling/compare-scheduling-priority.util';
import { SchedulingRescheduleService } from './scheduling-reschedule.service';

const makeBooking = (
  id: string,
  reference: string,
  extra: Record<string, unknown> = {},
) => ({
  id,
  reference,
  freightType: 'CONTAINER',
  cargoTotalWeightVgm: 100,
  scheduledDate: new Date('2026-06-20T08:00:00.000Z'),
  originYardId: 'yard-origin',
  destinationYardId: 'yard-destination',
  status: 'PAID',
  isGovernment: false,
  priorityScore: 50,
  bookingContainers: [
    {
      id: `${id}-line`,
      wagonsRequired: 5,
      quantity: 1,
      vgmPerUnitTons: 100,
    },
  ],
  ...extra,
});

describe('compareSchedulingPriority', () => {
  it('orders government before commercial', () => {
    const sorted = [
      {
        isGovernment: false,
        priorityScore: 50000,
        scheduledDate: new Date('2026-06-20'),
      },
      {
        isGovernment: true,
        priorityScore: 100,
        scheduledDate: new Date('2026-06-25'),
      },
    ].sort(compareSchedulingPriority);

    expect(sorted[0]?.isGovernment).toBe(true);
  });
});

describe('SchedulingRescheduleService', () => {
  let service: SchedulingRescheduleService;
  let trainSchedulesRepository: Record<string, jest.Mock>;
  let bookingsRepository: Record<string, jest.Mock>;
  let trainSchedulingService: Record<string, jest.Mock>;
  let schedulingRescheduleRepository: Record<string, jest.Mock>;

  beforeEach(() => {
    trainSchedulesRepository = {
      findByIdWithFullGraph: jest.fn(),
      updateStatus: jest.fn(),
    };
    bookingsRepository = {
      findByIdsForScheduling: jest.fn(),
      updateSchedulingFields: jest.fn(),
    };
    trainSchedulingService = {
      previewTrainSchedule: jest.fn(),
      unassignBooking: jest.fn(),
      assignBookingsToSchedule: jest.fn(),
    };
    schedulingRescheduleRepository = {
      createEvent: jest.fn().mockResolvedValue({ id: 'event-1' }),
    };

    service = new SchedulingRescheduleService(
      trainSchedulesRepository as never,
      bookingsRepository as never,
      trainSchedulingService as never,
      schedulingRescheduleRepository as never,
    );
  });

  it('rejects reschedule on dispatched trains', async () => {
    trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
      id: 'sched-1',
      status: 'DISPATCHED',
      scheduleBookings: [],
    });

    await expect(
      service.previewReschedule('sched-1', {
        incomingBookingIds: ['gov-1'],
        trigger: 'GOVERNMENT_PREEMPT',
      }),
    ).rejects.toBeInstanceOf(BadRequestException);
  });

  it('displaces lower-priority commercial when government incoming exceeds capacity', async () => {
    const commercial = makeBooking('c1', 'BKG-COMM', { priorityScore: 10, isGovernment: false });
    const government = makeBooking('g1', 'BKG-GOV', {
      isGovernment: true,
      priorityScore: 60000,
      governmentInstitution: 'Ministry',
    });

    trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
      id: 'sched-1',
      status: 'DRAFT',
      scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
      originStationId: 'yard-origin',
      destinationStationId: 'yard-destination',
      scheduleBookings: [{ bookingId: 'c1', booking: commercial }],
    });
    bookingsRepository.findByIdsForScheduling.mockResolvedValue([government]);

    trainSchedulingService.previewTrainSchedule.mockImplementation(
      async ({ bookingIds }: { bookingIds: string[] }) => ({
        valid: bookingIds.length <= 1,
        violations: bookingIds.length > 1 ? ['Train capacity exceeded'] : [],
        warnings: [],
      }),
    );

    const plan = await service.previewReschedule('sched-1', {
      incomingBookingIds: ['g1'],
      trigger: 'GOVERNMENT_PREEMPT',
    });

    expect(plan.retained.map((b) => b.id)).toEqual(['g1']);
    expect(plan.displaced.map((b) => b.id)).toEqual(['c1']);
    expect(plan.finalBookingIds).toEqual(['g1']);
  });

  it('readmits high-priority commercial when spare capacity remains', async () => {
    const low = makeBooking('c-low', 'BKG-LOW', { priorityScore: 5 });
    const high = makeBooking('c-high', 'BKG-HIGH', { priorityScore: 500 });
    const government = makeBooking('g1', 'BKG-GOV', { isGovernment: true, priorityScore: 60000 });

    trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue({
      id: 'sched-1',
      status: 'DRAFT',
      scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
      originStationId: 'yard-origin',
      destinationStationId: 'yard-destination',
      scheduleBookings: [
        { bookingId: 'c-low', booking: low },
        { bookingId: 'c-high', booking: high },
      ],
    });
    bookingsRepository.findByIdsForScheduling.mockResolvedValue([government]);

    const fitAttempts = new Map<string, number>();
    trainSchedulingService.previewTrainSchedule.mockImplementation(
      async ({ bookingIds }: { bookingIds: string[] }) => {
        const key = [...bookingIds].sort().join(',');
        const attempt = (fitAttempts.get(key) ?? 0) + 1;
        fitAttempts.set(key, attempt);

        const fits =
          bookingIds.length === 1 ||
          (key === 'c-high,g1' && attempt > 1);

        return {
          valid: fits,
          violations: fits ? [] : ['Train capacity exceeded'],
          warnings: [],
        };
      },
    );

    const plan = await service.previewReschedule('sched-1', {
      incomingBookingIds: ['g1'],
      trigger: 'GOVERNMENT_PREEMPT',
    });

    expect(plan.retained.map((b) => b.id)).toEqual(['g1']);
    expect(plan.readmitted.map((b) => b.id)).toEqual(['c-high']);
    expect(plan.displaced.map((b) => b.id)).toEqual(['c-low']);
    expect(plan.finalBookingIds).toEqual(['g1', 'c-high']);
  });

  it('maintenance reschedule updates departure and rebalances bookings', async () => {
    const commercial = makeBooking('c1', 'BKG-COMM', { priorityScore: 10 });
    const schedule = {
      id: 'sched-1',
      status: 'DRAFT',
      scheduledDepartureDate: new Date('2026-06-20T08:00:00.000Z'),
      originStationId: 'yard-origin',
      destinationStationId: 'yard-destination',
      scheduleBookings: [{ bookingId: 'c1', booking: commercial }],
    };

    trainSchedulesRepository.findByIdWithFullGraph.mockResolvedValue(schedule);
    bookingsRepository.findByIdsForScheduling.mockResolvedValue([commercial]);
    trainSchedulingService.previewTrainSchedule.mockResolvedValue({
      valid: true,
      violations: [],
      warnings: [],
    });
    trainSchedulesRepository.updateStatus.mockResolvedValue(undefined);
    trainSchedulingService.assignBookingsToSchedule.mockResolvedValue({ id: 'sched-1' });

    const result = await service.maintenanceReschedule(
      'sched-1',
      {
        incomingBookingIds: ['c1'],
        trigger: 'TRAIN_MAINTENANCE',
        reason: 'Locomotive service',
        newDepartureDate: '2026-06-22T10:00:00.000Z',
      },
      'staff-1',
    );

    expect(trainSchedulesRepository.updateStatus).toHaveBeenCalledWith(
      'sched-1',
      'DRAFT',
      { scheduledDepartureDate: new Date('2026-06-22T10:00:00.000Z') },
    );
    expect(schedulingRescheduleRepository.createEvent).toHaveBeenCalledWith(
      expect.objectContaining({
        trigger: 'TRAIN_MAINTENANCE',
        actorUserId: 'staff-1',
        reason: 'Locomotive service',
      }),
    );
    expect(result.plan.trigger).toBe('TRAIN_MAINTENANCE');
    expect(result.plan.finalBookingIds).toEqual(['c1']);
  });
});
