/**
 * Booking-type coverage — ROUND_TRIP, TRANSIT, ROUND_TRIP_TRANSIT end-to-end through
 * GuestBookingService.createGuestBooking(). ONE_WAY is already covered by
 * stop-based-booking-segment.e2e-spec.ts / checkin-cutoff.e2e-spec.ts and is not repeated
 * here.
 *
 * TRANSIT/ROUND_TRIP_TRANSIT use seed-core's single A(seq1)->B(seq2)->C(seq3) route for the
 * outbound/leg2 direction (transit at B). ROUND_TRIP/ROUND_TRIP_TRANSIT additionally need a
 * genuine return direction — createGuestRoundTripBooking does NOT validate stop-sequence
 * ordering between origin/destination (confirmed by reading the method), so it would silently
 * accept a "return" leg on the same forward route, but that's not what a real round trip is.
 * A local reverse route (C(seq1)->B(seq2)->A(seq3)) is created per-test instead, keeping
 * seed-core.ts itself untouched (this reverse route isn't a general-purpose fixture need).
 *
 * Uses the same harness/Tier-2 pattern as reserve-seat-issue-booking.e2e-spec.ts and
 * stop-based-booking-segment.e2e-spec.ts.
 */
import { IdDocumentType, PassengerCategory } from "@prisma/client";
import { SchedulesService } from "../src/modules/schedules/schedules.service";
import { SeatsService } from "../src/modules/seats/seats.service";
import { CurrencyService } from "../src/modules/currency/currency.service";
import { FareEngineService } from "../src/modules/fare-engine/fare-engine.service";
import { SystemConfigService } from "../src/modules/system-config/system-config.service";
import { GuestBookingService } from "../src/modules/bookings/guest-booking.service";
import { createServiceHarness, ServiceHarness } from "./setup/slim-app";
import { IDS, DISTANCE, resetAndSeedCore } from "./fixtures/seed-core";

function asyncStub(): any {
  return new Proxy({}, { get: () => async () => undefined });
}

describe("Booking types — ROUND_TRIP / TRANSIT / ROUND_TRIP_TRANSIT", () => {
  let harness: ServiceHarness;
  let schedulesService: SchedulesService;
  let seatsService: SeatsService;
  let guestBookingService: GuestBookingService;
  let reverseRouteId: string;

  beforeAll(async () => {
    harness = await createServiceHarness();
    schedulesService = await harness.moduleRef.resolve(SchedulesService);
    const currencyService = harness.moduleRef.get(CurrencyService);
    const fareEngine = harness.moduleRef.get(FareEngineService);
    const systemConfig = new SystemConfigService(harness.prisma as any);

    seatsService = new SeatsService(harness.prisma as any, asyncStub(), systemConfig, asyncStub(), asyncStub());
    guestBookingService = new GuestBookingService(
      harness.prisma as any,
      seatsService,
      asyncStub(), // verifaydaService — test passengers use PASSPORT, not NATIONAL_ID
      currencyService,
      asyncStub(), // passengerAuthService — no createAccount in these DTOs
      fareEngine,
      { emit: () => true } as any, // eventEmitter
      asyncStub(), // paymentsService — not reached by createGuestBooking
      asyncStub(), // auditService
      asyncStub(), // smsClient
    );
  });

  afterAll(async () => {
    await harness?.close();
  });

  beforeEach(async () => {
    await resetAndSeedCore(harness.prisma);
    reverseRouteId = await createReverseRoute();
  });

  /** Mirrors seed-core's A->B->C route but reversed (C->B->A), for a genuine return leg. */
  async function createReverseRoute(): Promise<string> {
    const route = await harness.prisma.route.create({
      data: {
        code: `RT-REV-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
        name: "Reverse Line",
        effectiveFrom: new Date("2020-01-01T00:00:00.000Z"),
        active: true,
        stops: {
          create: [
            { stationId: IDS.stationC, sequence: 1, distanceKm: 0 },
            { stationId: IDS.stationB, sequence: 2, distanceKm: DISTANCE.C - DISTANCE.B },
            { stationId: IDS.stationA, sequence: 3, distanceKm: DISTANCE.C - DISTANCE.A },
          ],
        },
      },
    });
    return route.id;
  }

  /** Coach assigned via coachIds at creation time — createSchedule rejects zero coaches. */
  async function createTestSchedule(opts: { trainNumber: string; routeId: string; departureAt: Date; arrivalAt: Date }) {
    const train = await harness.prisma.train.create({
      data: { number: opts.trainNumber, name: `Test ${opts.trainNumber}` },
    });
    const coach = await harness.prisma.coach.create({
      data: { coachTypeId: IDS.coachType, number: `${opts.trainNumber}-C1`, capacity: 6, sequence: 1, status: "ACTIVE" },
    });
    const seats = await Promise.all(
      ["1A", "1B", "1C", "1D", "1E", "1F"].map((seatNumber, i) =>
        harness.prisma.seat.create({
          data: { coachId: coach.id, seatNumber, row: 1, col: seatNumber.slice(-1), isWindow: i === 0, isAisle: i === 1 },
        }),
      ),
    );
    const schedule = await schedulesService.createSchedule({
      trainId: train.id,
      routeId: opts.routeId,
      departureAt: opts.departureAt.toISOString(),
      arrivalAt: opts.arrivalAt.toISOString(),
      coachIds: [coach.id],
    } as any);
    return { schedule, seats };
  }

  async function hold(scheduleId: string, originStationId: string, destinationStationId: string, seatId: string, passengerId: string) {
    return seatsService.holdSeats({
      scheduleId,
      originStationId,
      destinationStationId,
      passengers: [{ passengerId, seatId }],
    } as any);
  }

  /** Holds several seats on the SAME leg in one SeatHold row (one holdId covers them all) —
   * required when a single booking carries multiple travelers on one leg: holdSeats rejects a
   * second call for the same passengerId+leg as a conflict, so multi-passenger holds must be
   * one call with distinct passengerIds, not N sequential single-seat calls. */
  async function holdMany(scheduleId: string, originStationId: string, destinationStationId: string, seatIds: string[]) {
    return seatsService.holdSeats({
      scheduleId,
      originStationId,
      destinationStationId,
      passengers: seatIds.map((seatId, i) => ({ passengerId: `44444444-4444-4444-8444-40000000000${i}`, seatId })),
    } as any);
  }

  function passenger(overrides: Partial<Record<string, any>> = {}) {
    return {
      passengerName: "Test Traveler",
      dateOfBirth: "1990-01-01",
      idDocumentType: IdDocumentType.PASSPORT,
      passportNumber: "X123456",
      passportCountry: "Djibouti",
      nationality: "Djiboutian",
      ...overrides,
    };
  }

  const future = (mins: number) => new Date(Date.now() + mins * 60_000);

  describe("ROUND_TRIP", () => {
    it("creates a booking with correct fields for both legs", async () => {
      const dep = future(180);
      const arr = future(280);
      const { schedule: outbound, seats: outSeats } = await createTestSchedule({ trainNumber: `RT-OB-${Date.now()}`, routeId: IDS.route, departureAt: dep, arrivalAt: arr });
      const { schedule: ret, seats: retSeats } = await createTestSchedule({ trainNumber: `RT-RET-${Date.now()}`, routeId: reverseRouteId, departureAt: future(400), arrivalAt: future(480) });

      const outHold = await hold(outbound.id, IDS.stationA, IDS.stationC, outSeats[0].id, "11111111-1111-4111-8111-100000000001");
      const retHold = await hold(ret.id, IDS.stationC, IDS.stationA, retSeats[0].id, "11111111-1111-4111-8111-100000000001");

      const result: any = await guestBookingService.createGuestBooking({
        bookingType: "ROUND_TRIP",
        scheduleId: outbound.id,
        holdId: (outHold as any).holdId,
        originStationId: IDS.stationA,
        destinationStationId: IDS.stationC,
        seatClassId: IDS.seatClassLocal,
        returnScheduleId: ret.id,
        returnHoldId: (retHold as any).holdId,
        returnOriginStationId: IDS.stationC,
        returnDestinationStationId: IDS.stationA,
        passengers: [passenger({ seatId: outSeats[0].id, returnSeatId: retSeats[0].id })],
      } as any);

      expect(result.bookingType).toBe("ROUND_TRIP");
      expect(result.returnScheduleId).toBe(ret.id);
      expect(result.returnOriginStationId).toBe(IDS.stationC);
      expect(result.returnDestinationStationId).toBe(IDS.stationA);
      expect(result.totalMinor).toBeGreaterThan(0);

      const seatRows = await harness.prisma.bookingSeat.findMany({ where: { bookingId: result.id } });
      expect(seatRows).toHaveLength(2); // one BookingSeat per leg
      expect(seatRows.map((s) => s.scheduleId).sort()).toEqual([outbound.id, ret.id].sort());
    });

    it("rejects a ROUND_TRIP missing return fields with the exact message", async () => {
      const { schedule: outbound, seats } = await createTestSchedule({ trainNumber: `RT-MISS-${Date.now()}`, routeId: IDS.route, departureAt: future(180), arrivalAt: future(280) });
      const outHold = await hold(outbound.id, IDS.stationA, IDS.stationC, seats[0].id, "11111111-1111-4111-8111-100000000002");

      await expect(
        guestBookingService.createGuestBooking({
          bookingType: "ROUND_TRIP",
          scheduleId: outbound.id,
          holdId: (outHold as any).holdId,
          originStationId: IDS.stationA,
          destinationStationId: IDS.stationC,
          seatClassId: IDS.seatClassLocal,
          passengers: [passenger({ seatId: seats[0].id })],
        } as any),
      ).rejects.toThrow(/returnScheduleId, returnHoldId, returnOriginStationId and returnDestinationStationId are required/i);
    });

    // NOTE: a "first child travels free" test was attempted here and dropped — the
    // fareMinor-by-seatId-presence branch (`p.seatId ? childUnitFare : 0`, commented "Free
    // children have no seat (frontend excludes them from the DTO)") is UNREACHABLE in
    // practice: every passenger row unconditionally goes through
    // `seat: { connect: { id: p.seatId } } }` when the booking is created (confirmed in
    // ONE_WAY at line ~367, ROUND_TRIP at ~874, TRANSIT at ~1075) — a passenger with no
    // `seatId` makes that Prisma `connect` throw, so a truly seat-less "free child" can never
    // reach the booking-creation step at all. FLAGGED, not fixed — see the final report.

    it("two adults on the same booking are each charged the full per-leg fare", async () => {
      const dep = future(180);
      const { schedule: outbound, seats: outSeats } = await createTestSchedule({ trainNumber: `RT-2ADULT-OB-${Date.now()}`, routeId: IDS.route, departureAt: dep, arrivalAt: future(280) });
      const { schedule: ret, seats: retSeats } = await createTestSchedule({ trainNumber: `RT-2ADULT-RET-${Date.now()}`, routeId: reverseRouteId, departureAt: future(400), arrivalAt: future(480) });

      const outHold = await holdMany(outbound.id, IDS.stationA, IDS.stationC, [outSeats[0].id, outSeats[1].id]);
      const retHold = await holdMany(ret.id, IDS.stationC, IDS.stationA, [retSeats[0].id, retSeats[1].id]);

      const result: any = await guestBookingService.createGuestBooking({
        bookingType: "ROUND_TRIP",
        scheduleId: outbound.id,
        holdId: (outHold as any).holdId,
        originStationId: IDS.stationA,
        destinationStationId: IDS.stationC,
        seatClassId: IDS.seatClassLocal,
        returnScheduleId: ret.id,
        returnHoldId: (retHold as any).holdId,
        returnOriginStationId: IDS.stationC,
        returnDestinationStationId: IDS.stationA,
        passengers: [
          passenger({ seatId: outSeats[0].id, returnSeatId: retSeats[0].id, passengerName: "Adult One" }),
          passenger({ seatId: outSeats[1].id, returnSeatId: retSeats[1].id, passengerName: "Adult Two" }),
        ],
      } as any);

      expect(result.adultCount).toBe(2);
      expect(result.childCount).toBe(0);
      const seatFares = await harness.prisma.bookingSeat.findMany({ where: { bookingId: result.id } });
      expect(seatFares).toHaveLength(4); // 2 passengers x 2 legs
      const fareMinors = new Set(seatFares.map((s) => s.fareMinor));
      expect(fareMinors.size).toBe(1); // every seat on every leg is the same base fare
      expect([...fareMinors][0]).toBeGreaterThan(0);
      const total = seatFares.reduce((sum, s) => sum + s.fareMinor, 0);
      expect(result.totalMinor).toBe(total);
    });
  });

  describe("TRANSIT", () => {
    it("creates a single booking spanning leg-1 and leg-2 via the transit station", async () => {
      const dep = future(180);
      const { schedule: leg1, seats: leg1Seats } = await createTestSchedule({ trainNumber: `TR-L1-${Date.now()}`, routeId: IDS.route, departureAt: dep, arrivalAt: future(220) });
      const { schedule: leg2, seats: leg2Seats } = await createTestSchedule({ trainNumber: `TR-L2-${Date.now()}`, routeId: IDS.route, departureAt: future(260), arrivalAt: future(320) });

      const leg1Hold = await hold(leg1.id, IDS.stationA, IDS.stationB, leg1Seats[0].id, "22222222-2222-4222-8222-200000000001");
      const leg2Hold = await hold(leg2.id, IDS.stationB, IDS.stationC, leg2Seats[0].id, "22222222-2222-4222-8222-200000000001");

      const result: any = await guestBookingService.createGuestBooking({
        bookingType: "TRANSIT",
        scheduleId: leg1.id,
        holdId: (leg1Hold as any).holdId,
        originStationId: IDS.stationA,
        destinationStationId: IDS.stationB,
        seatClassId: IDS.seatClassLocal,
        leg2ScheduleId: leg2.id,
        leg2HoldId: (leg2Hold as any).holdId,
        transitStationId: IDS.stationB,
        leg2DestinationStationId: IDS.stationC,
        passengers: [passenger({ seatId: leg1Seats[0].id, leg2SeatId: leg2Seats[0].id })],
      } as any);

      expect(result.bookingType).toBe("TRANSIT");
      expect(result.originStationId).toBe(IDS.stationA);
      expect(result.destinationStationId).toBe(IDS.stationC); // full journey span, not just leg-1
      expect(result.leg2ScheduleId).toBe(leg2.id);
      expect(result.leg2OriginStationId).toBe(IDS.stationB);
      expect(result.leg2DestinationStationId).toBe(IDS.stationC);

      const seatRows = await harness.prisma.bookingSeat.findMany({ where: { bookingId: result.id } });
      expect(seatRows).toHaveLength(2);
      expect(seatRows.map((s) => s.scheduleId).sort()).toEqual([leg1.id, leg2.id].sort());
    });

    it("rejects a TRANSIT missing leg-2 fields with the exact message", async () => {
      const { schedule: leg1, seats } = await createTestSchedule({ trainNumber: `TR-MISS-${Date.now()}`, routeId: IDS.route, departureAt: future(180), arrivalAt: future(220) });
      const leg1Hold = await hold(leg1.id, IDS.stationA, IDS.stationB, seats[0].id, "22222222-2222-4222-8222-200000000002");

      await expect(
        guestBookingService.createGuestBooking({
          bookingType: "TRANSIT",
          scheduleId: leg1.id,
          holdId: (leg1Hold as any).holdId,
          originStationId: IDS.stationA,
          destinationStationId: IDS.stationB,
          seatClassId: IDS.seatClassLocal,
          passengers: [passenger({ seatId: seats[0].id })],
        } as any),
      ).rejects.toThrow(/leg2ScheduleId, leg2HoldId, transitStationId and leg2DestinationStationId are required/i);
    });
  });

  describe("ROUND_TRIP_TRANSIT", () => {
    it("creates a single booking spanning all 4 legs (outbound x2, return x2)", async () => {
      const { schedule: obL1, seats: obL1Seats } = await createTestSchedule({ trainNumber: `RTT-OB1-${Date.now()}`, routeId: IDS.route, departureAt: future(180), arrivalAt: future(220) });
      const { schedule: obL2, seats: obL2Seats } = await createTestSchedule({ trainNumber: `RTT-OB2-${Date.now()}`, routeId: IDS.route, departureAt: future(260), arrivalAt: future(320) });
      const { schedule: retL1, seats: retL1Seats } = await createTestSchedule({ trainNumber: `RTT-RET1-${Date.now()}`, routeId: reverseRouteId, departureAt: future(500), arrivalAt: future(560) });
      const { schedule: retL2, seats: retL2Seats } = await createTestSchedule({ trainNumber: `RTT-RET2-${Date.now()}`, routeId: reverseRouteId, departureAt: future(600), arrivalAt: future(660) });

      const pid = "33333333-3333-4333-8333-300000000001";
      const obL1Hold = await hold(obL1.id, IDS.stationA, IDS.stationB, obL1Seats[0].id, pid);
      const obL2Hold = await hold(obL2.id, IDS.stationB, IDS.stationC, obL2Seats[0].id, pid);
      const retL1Hold = await hold(retL1.id, IDS.stationC, IDS.stationB, retL1Seats[0].id, pid);
      const retL2Hold = await hold(retL2.id, IDS.stationB, IDS.stationA, retL2Seats[0].id, pid);

      const result: any = await guestBookingService.createGuestBooking({
        bookingType: "ROUND_TRIP_TRANSIT",
        scheduleId: obL1.id,
        holdId: (obL1Hold as any).holdId,
        originStationId: IDS.stationA,
        destinationStationId: IDS.stationB,
        seatClassId: IDS.seatClassLocal,
        leg2ScheduleId: obL2.id,
        leg2HoldId: (obL2Hold as any).holdId,
        transitStationId: IDS.stationB,
        leg2DestinationStationId: IDS.stationC,
        returnScheduleId: retL1.id,
        returnHoldId: (retL1Hold as any).holdId,
        returnOriginStationId: IDS.stationC,
        returnDestinationStationId: IDS.stationB,
        returnLeg2ScheduleId: retL2.id,
        returnLeg2HoldId: (retL2Hold as any).holdId,
        returnTransitStationId: IDS.stationB,
        returnLeg2DestinationStationId: IDS.stationA,
        passengers: [passenger({
          seatId: obL1Seats[0].id,
          leg2SeatId: obL2Seats[0].id,
          returnSeatId: retL1Seats[0].id,
          returnLeg2SeatId: retL2Seats[0].id,
        })],
      } as any);

      expect(result.bookingType).toBe("ROUND_TRIP_TRANSIT");
      const seatRows = await harness.prisma.bookingSeat.findMany({ where: { bookingId: result.id } });
      expect(seatRows).toHaveLength(4);
      expect(seatRows.map((s) => s.scheduleId).sort()).toEqual([obL1.id, obL2.id, retL1.id, retL2.id].sort());
    });

    it("rejects a ROUND_TRIP_TRANSIT missing any leg's fields with the exact message", async () => {
      const { schedule: obL1, seats } = await createTestSchedule({ trainNumber: `RTT-MISS-${Date.now()}`, routeId: IDS.route, departureAt: future(180), arrivalAt: future(220) });
      const obL1Hold = await hold(obL1.id, IDS.stationA, IDS.stationB, seats[0].id, "33333333-3333-4333-8333-300000000002");

      await expect(
        guestBookingService.createGuestBooking({
          bookingType: "ROUND_TRIP_TRANSIT",
          scheduleId: obL1.id,
          holdId: (obL1Hold as any).holdId,
          originStationId: IDS.stationA,
          destinationStationId: IDS.stationB,
          seatClassId: IDS.seatClassLocal,
          passengers: [passenger({ seatId: seats[0].id })],
        } as any),
      ).rejects.toThrow(/ROUND_TRIP_TRANSIT requires all 4 holds and all transit\/return station fields/i);
    });
  });
});
