import { TrainSchedulingService } from './train-scheduling.service';
import { Booking } from '../bookings/entities/booking.entity';
import { ClearanceMilestone } from '../contracts/entities/clearance-milestone.entity';

type Row = Pick<ClearanceMilestone, 'bookingId' | 'milestoneCode' | 'status'> & {
  metadata?: Record<string, unknown> | null;
  triggeredAt?: Date | null;
};

/**
 * The gate pass is secured once per train schedule, but each booking only earns
 * its GATEPASS_GRANTED milestone after settling freight payment. An unpaid
 * booking must not ride a paid neighbour's grant — the train proceeds, that
 * booking stays pending.
 */
function makeService(bookings: Array<Partial<Booking>>, rows: Row[]) {
  const milestoneRepo = {
    find: jest.fn().mockResolvedValue(rows),
    save: jest.fn((row: Row) => Promise.resolve(row)),
  };
  const bookingRepo = { find: jest.fn().mockResolvedValue(bookings) };
  const dataSource = {
    getRepository: (entity: unknown) =>
      entity === Booking ? bookingRepo : milestoneRepo,
  };

  const service = Object.create(
    TrainSchedulingService.prototype,
  ) as TrainSchedulingService;
  Object.assign(service, {
    dataSource,
    logger: { warn: jest.fn(), log: jest.fn() },
  });
  return { service, milestoneRepo };
}

/** Reach the private bridge write under test. */
function grant(service: TrainSchedulingService, at: Date): Promise<void> {
  return (
    service as unknown as {
      completeGatepassMilestoneForSchedule(id: string, at: Date): Promise<void>;
    }
  ).completeGatepassMilestoneForSchedule('sched-1', at);
}

const securedAt = new Date('2026-07-09T08:00:00.000Z');

describe('gate pass is withheld from bookings that have not paid freight', () => {
  it('grants the paid booking and leaves the unpaid one pending', async () => {
    const rows: Row[] = [
      { bookingId: 'paid', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'COMPLETED' },
      { bookingId: 'paid', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
      { bookingId: 'unpaid', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'PENDING' },
      { bookingId: 'unpaid', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
    ];
    const { service, milestoneRepo } = makeService(
      [
        { id: 'paid', status: 'CONFIRMED', paymentStatus: 'PENDING' },
        { id: 'unpaid', status: 'CONFIRMED', paymentStatus: 'PENDING' },
      ],
      rows,
    );

    await grant(service, securedAt);

    const saved = milestoneRepo.save.mock.calls.map(([r]: [Row]) => r);
    expect(saved).toHaveLength(1);
    expect(saved[0]!.bookingId).toBe('paid');
    expect(saved[0]!.status).toBe('COMPLETED');
    expect(saved[0]!.triggeredAt).toBe(securedAt);

    const unpaid = rows.find(
      (r) => r.bookingId === 'unpaid' && r.milestoneCode === 'GATEPASS_GRANTED',
    );
    expect(unpaid!.status).toBe('PENDING');
  });

  it('treats a booking paid outside the milestone path as paid', async () => {
    // Some payment paths settle the invoice without writing the milestone; the
    // clearance views self-heal it on read, so the gate pass must not lag.
    const rows: Row[] = [
      { bookingId: 'b-1', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'PENDING' },
      { bookingId: 'b-1', milestoneCode: 'GATEPASS_GRANTED', status: 'PENDING' },
    ];
    const { service, milestoneRepo } = makeService(
      [{ id: 'b-1', status: 'CONFIRMED', paymentStatus: 'PAID' }],
      rows,
    );

    await grant(service, securedAt);

    expect(milestoneRepo.save).toHaveBeenCalledTimes(1);
    expect(milestoneRepo.save.mock.calls[0]![0].bookingId).toBe('b-1');
  });

  it('leaves an already-granted milestone untouched', async () => {
    const rows: Row[] = [
      { bookingId: 'b-1', milestoneCode: 'FREIGHT_PAYMENT_SETTLED', status: 'COMPLETED' },
      { bookingId: 'b-1', milestoneCode: 'GATEPASS_GRANTED', status: 'COMPLETED' },
    ];
    const { service, milestoneRepo } = makeService(
      [{ id: 'b-1', status: 'PAID', paymentStatus: 'PAID' }],
      rows,
    );

    await grant(service, securedAt);

    expect(milestoneRepo.save).not.toHaveBeenCalled();
  });

  it('does nothing when the schedule carries no customs bookings', async () => {
    const { service, milestoneRepo } = makeService([], []);

    await grant(service, securedAt);

    expect(milestoneRepo.save).not.toHaveBeenCalled();
  });
});
