/**
 * Stop-based (mid-route) booking — segment correctness suite.
 *
 * Regression coverage for three bugs reported against live stop-based bookings:
 *
 *   1. Search results (SearchService.buildScheduleResult) showed the train's overall
 *      departure/arrival instead of the selected origin/destination stop's own time — e.g.
 *      searching B→C on a A→B→C schedule showed A's departure time, not B's. Rooted in
 *      resolving the boarding/alighting STATION correctly for a mid-route segment while still
 *      reading TIME off the schedule's full-route span. Fixed via resolveBookingSegment() (also
 *      used by BookingsService, TicketsService, NotificationsService) — see
 *      src/common/utils/segment-resolver.utils.ts.
 *   2. GuestBookingService's 30-minute booking cutoff was computed off the train's origin
 *      departure regardless of where the passenger actually boards, so a schedule whose origin
 *      had already departed >30min ago wrongly blocked booking a downstream segment that
 *      hadn't closed yet.
 *   3. Even after (2), GuestBookingService still enforced a hardcoded, non-configurable 30
 *      minutes — ignoring RouteStop/Route.checkinMinutesBefore, the SAME configurable cutoff
 *      that SeatsService.holdSeats and the search step already enforce. A passenger who passed
 *      the earlier steps under a shorter (or longer) CONFIGURED cutoff could still be wrongly
 *      rejected — or wrongly allowed — at /booking/review with "not accepted within 30 minutes
 *      of departure". Fixed by having GuestBookingService use the same resolveCheckinCutoff()
 *      utility as SeatsService.holdSeats and SearchService — see
 *      src/common/utils/checkin-cutoff.utils.ts.
 *
 * Uses the slim harness (real Nest DI) for SchedulesService — this exercises the actual
 * cumulative travel-time interpolation in SchedulesService.createSchedule, same as
 * checkin-cutoff.e2e-spec.ts. SeatsService/SearchService/BookingsService/GuestBookingService
 * are NOT in the slim harness's DOMAIN_MODULES (they pull in NotificationsModule → RabbitMQ),
 * so they're instantiated directly with a real Prisma + stubbed collaborators, mirroring the
 * Tier-2 pattern in money-integrity.e2e-spec.ts.
 */
import { IdDocumentType } from "@prisma/client";
import { SchedulesService } from "../src/modules/schedules/schedules.service";
import { SeatsService } from "../src/modules/seats/seats.service";
import { SegmentsService } from "../src/modules/segments/segments.service";
import { SearchService } from "../src/modules/search/search.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 { BookingsService } from "../src/modules/bookings/bookings.service";
import { GuestBookingService } from "../src/modules/bookings/guest-booking.service";
import { createServiceHarness, ServiceHarness } from "./setup/slim-app";
import { IDS, resetAndSeedCore } from "./fixtures/seed-core";

/** A Proxy whose every property is an async no-op — satisfies unused collaborator method calls. */
function asyncStub(): any {
  return new Proxy({}, { get: () => async () => undefined });
}

/** Formats a Date as a YYYY-MM-DD string in the process's local timezone (EAT on this host —
 * matches search.service.ts's "+03:00" date-matching window). */
function localDateStr(d: Date): string {
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, "0");
  const day = String(d.getDate()).padStart(2, "0");
  return `${y}-${m}-${day}`;
}

describe("Stop-based booking — segment time & cutoff correctness", () => {
  let harness: ServiceHarness;
  let schedulesService: SchedulesService;
  let seatsService: SeatsService;
  let searchService: SearchService;
  let bookingsService: BookingsService;
  let guestBookingService: GuestBookingService;

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

    searchService = new SearchService(harness.prisma as any, currencyService, fareEngine, segmentsService);
    // holdSeats() itself never touches segmentsService (only getSeatMap/availability-map
    // callers do), so stubbing it here is safe — mirrors checkin-cutoff.e2e-spec.ts.
    seatsService = new SeatsService(harness.prisma as any, asyncStub(), systemConfig, asyncStub(), asyncStub());
    bookingsService = new BookingsService(
      harness.prisma as any,
      asyncStub(), // dataSource
      seatsService,
      { emit: () => true } as any, // eventEmitter
      asyncStub(), // verifaydaService
      currencyService,
      fareEngine,
      asyncStub(), // auditService
    );
    guestBookingService = new GuestBookingService(
      harness.prisma as any,
      seatsService,
      asyncStub(), // verifaydaService — never reached: test passengers use PASSPORT, not NATIONAL_ID
      currencyService,
      asyncStub(), // passengerAuthService — never reached: no createAccount in these DTOs
      fareEngine,
      { emit: () => true } as any, // eventEmitter
      asyncStub(), // paymentsService — never reached: these tests use createGuestBooking, not issueBookingFromReservation
      asyncStub(), // auditService
      asyncStub(), // smsClient
    );
  });

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

  /** Creates a fresh Train + TrainSchedule on the seed-core route, coach assigned at creation
   * (createSchedule now rejects a schedule with zero coaches). */
  async function createTestSchedule(opts: { trainNumber: 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: 4, sequence: 1, status: "ACTIVE" },
    });
    const seats = await Promise.all(
      ["1A", "1B", "1C", "1D"].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: IDS.route,
      departureAt: opts.departureAt.toISOString(),
      arrivalAt: opts.arrivalAt.toISOString(),
      coachIds: [coach.id],
    } as any);
    return { schedule, seats };
  }

  function foreignPassenger(seatId: string) {
    return {
      seatId,
      passengerName: "Test Passenger",
      dateOfBirth: "1990-01-01",
      idDocumentType: IdDocumentType.PASSPORT,
      passportNumber: "X123456",
      passportCountry: "Djibouti",
      nationality: "Djiboutian",
    };
  }

  describe("search results (SearchService.searchTrips)", () => {
    it("shows the boarding stop's own departure time, not the schedule's full-route (station A) departure", async () => {
      await resetAndSeedCore(harness.prisma);
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 60 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      const dep = new Date(Date.now() + 3 * 60 * 60_000); // A's departure, 3h out
      const arr = new Date(dep.getTime() + 100 * 60_000); // C's arrival
      const { schedule } = await createTestSchedule({ trainNumber: `SEG-DEP-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const result: any = await searchService.searchTrips({
        originStationId: IDS.stationB,
        destinationStationId: IDS.stationC,
        date: localDateStr(dep),
        adultCount: 1,
      } as any);

      const found = result.outbound.find((o: any) => o.scheduleId === schedule.id);
      expect(found).toBeTruthy();

      const expectedBDeparture = new Date(dep.getTime() + 60 * 60_000);
      expect(new Date(found.departureAt).getTime()).toBe(expectedBDeparture.getTime());
      // Would equal A's departure (`dep`) under the old (buggy) schedule.departureAt fallback.
      expect(new Date(found.departureAt).getTime()).not.toBe(dep.getTime());
    });

    it("shows the alighting stop's own arrival time, not the schedule's full-route (station C) arrival", async () => {
      await resetAndSeedCore(harness.prisma);
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 60 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      const dep = new Date(Date.now() + 3 * 60 * 60_000);
      const arr = new Date(dep.getTime() + 100 * 60_000); // C's arrival
      const { schedule } = await createTestSchedule({ trainNumber: `SEG-ARR-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const result: any = await searchService.searchTrips({
        originStationId: IDS.stationA,
        destinationStationId: IDS.stationB,
        date: localDateStr(dep),
        adultCount: 1,
      } as any);

      const found = result.outbound.find((o: any) => o.scheduleId === schedule.id);
      expect(found).toBeTruthy();

      const expectedBArrival = new Date(dep.getTime() + 60 * 60_000);
      expect(new Date(found.arrivalAt).getTime()).toBe(expectedBArrival.getTime());
      // Would equal C's arrival (`arr`) under the old (buggy) schedule.arrivalAt fallback.
      expect(new Date(found.arrivalAt).getTime()).not.toBe(arr.getTime());
    });
  });

  describe("guest booking cutoff (GuestBookingService.createGuestBooking)", () => {
    it("does NOT block booking a downstream segment whose own boarding stop is still far out, even though the schedule's origin already departed", async () => {
      await resetAndSeedCore(harness.prisma);
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 150 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      // A departs in 5min (already inside a naive 30-min-before-departure cutoff), but B — the
      // passenger's actual boarding stop — is A+150min out (~2.5h), comfortably clear.
      const dep = new Date(Date.now() + 5 * 60_000);
      const arr = new Date(dep.getTime() + 190 * 60_000);
      const { schedule, seats } = await createTestSchedule({ trainNumber: `CUTOFF-OK-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const hold = await seatsService.holdSeats({
        scheduleId: schedule.id,
        originStationId: IDS.stationB,
        destinationStationId: IDS.stationC,
        passengers: [{ passengerId: "66666666-6666-4666-8666-666666666666", seatId: seats[0].id }],
      } as any);

      const booking: any = await guestBookingService.createGuestBooking({
        scheduleId: schedule.id,
        holdId: (hold as any).holdId,
        originStationId: IDS.stationB,
        destinationStationId: IDS.stationC,
        seatClassId: IDS.seatClassLocal,
        passengers: [foreignPassenger(seats[0].id)],
      } as any);

      expect(booking.bookingRef).toBeTruthy();
      expect(booking.originStationId).toBe(IDS.stationB);
      expect(booking.destinationStationId).toBe(IDS.stationC);
    });

    it("still blocks booking when the passenger's own boarding stop is itself within 30 minutes of its departure", async () => {
      await resetAndSeedCore(harness.prisma);
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 10 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      // B departs at dep+10min (~15min from now) — inside the 30-min cutoff. Hold is created
      // directly (bypassing SeatsService.holdSeats' own, separately-tested arrival-based
      // cutoff — see checkin-cutoff.e2e-spec.ts) to isolate GuestBookingService's own check.
      const dep = new Date(Date.now() + 5 * 60_000);
      const arr = new Date(dep.getTime() + 50 * 60_000);
      const { schedule, seats } = await createTestSchedule({ trainNumber: `CUTOFF-BLOCK-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const hold = await harness.prisma.seatHold.create({
        data: {
          scheduleId: schedule.id,
          seatIds: [seats[0].id],
          passengerId: "77777777-7777-4777-8777-777777777777",
          expiresAt: new Date(Date.now() + 10 * 60_000),
        },
      });

      await expect(
        guestBookingService.createGuestBooking({
          scheduleId: schedule.id,
          holdId: hold.id,
          originStationId: IDS.stationB,
          destinationStationId: IDS.stationC,
          seatClassId: IDS.seatClassLocal,
          passengers: [foreignPassenger(seats[0].id)],
        } as any),
      ).rejects.toThrow(/not accepted within 30 minutes/i);
    });

    it("honors a stop-level checkinMinutesBefore override SHORTER than 30 minutes — booking succeeds inside the old hardcoded window", async () => {
      // Regression for the reported bug: /booking/review still rejected a booking with
      // "not accepted within 30 minutes of departure" even after the passenger passed the
      // earlier steps under a shorter CONFIGURED cutoff — because createGuestBooking used to
      // enforce its own separate, hardcoded 30 minutes regardless of RouteStop/Route
      // .checkinMinutesBefore. B's own configured cutoff here is 10 minutes.
      await resetAndSeedCore(harness.prisma, { B: { checkinMinutesBefore: 10 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 20 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      // B departs at dep+20min (~25min from now) — inside the OLD hardcoded 30-min cutoff,
      // but outside B's own configured 10-min cutoff.
      const dep = new Date(Date.now() + 5 * 60_000);
      const arr = new Date(dep.getTime() + 60 * 60_000);
      const { schedule, seats } = await createTestSchedule({ trainNumber: `CUTOFF-CFG-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const hold = await seatsService.holdSeats({
        scheduleId: schedule.id,
        originStationId: IDS.stationB,
        destinationStationId: IDS.stationC,
        passengers: [{ passengerId: "88888888-8888-4888-8888-888888888888", seatId: seats[0].id }],
      } as any);

      const booking: any = await guestBookingService.createGuestBooking({
        scheduleId: schedule.id,
        holdId: (hold as any).holdId,
        originStationId: IDS.stationB,
        destinationStationId: IDS.stationC,
        seatClassId: IDS.seatClassLocal,
        passengers: [foreignPassenger(seats[0].id)],
      } as any);

      expect(booking.bookingRef).toBeTruthy();
    });

    it("honors a stop-level checkinMinutesBefore override LONGER than 30 minutes — still blocks past the old hardcoded window", async () => {
      await resetAndSeedCore(harness.prisma, { B: { checkinMinutesBefore: 90 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 40 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      // B departs at dep+40min (~45min from now) — outside the OLD hardcoded 30-min cutoff
      // (would have wrongly been allowed), but inside B's own configured 90-min cutoff.
      const dep = new Date(Date.now() + 5 * 60_000);
      const arr = new Date(dep.getTime() + 80 * 60_000);
      const { schedule, seats } = await createTestSchedule({ trainNumber: `CUTOFF-CFG2-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const hold = await harness.prisma.seatHold.create({
        data: {
          scheduleId: schedule.id,
          seatIds: [seats[0].id],
          passengerId: "99999999-9999-4999-8999-999999999999",
          expiresAt: new Date(Date.now() + 10 * 60_000),
        },
      });

      await expect(
        guestBookingService.createGuestBooking({
          scheduleId: schedule.id,
          holdId: hold.id,
          originStationId: IDS.stationB,
          destinationStationId: IDS.stationC,
          seatClassId: IDS.seatClassLocal,
          passengers: [foreignPassenger(seats[0].id)],
        } as any),
      ).rejects.toThrow(/not accepted within 90 minutes/i);
    });
  });

  describe("booking detail & list segment resolution (BookingsService)", () => {
    it("getByRef and findByPassengerId show the boarding stop's own time and station, not the schedule's full-route span", async () => {
      await resetAndSeedCore(harness.prisma);
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 2 } }, data: { travelMinutesToStop: 60 } });
      await harness.prisma.routeStop.update({ where: { routeId_sequence: { routeId: IDS.route, sequence: 3 } }, data: { travelMinutesToStop: 40 } });

      const dep = new Date(Date.now() + 3 * 60 * 60_000);
      const arr = new Date(dep.getTime() + 100 * 60_000);
      const { schedule } = await createTestSchedule({ trainNumber: `SEG-DETAIL-${Date.now()}`, departureAt: dep, arrivalAt: arr });

      const passenger = await harness.prisma.passenger.create({ data: {} });
      const booking = await harness.prisma.booking.create({
        data: {
          bookingRef: `SEGDET${Date.now()}`,
          passengerId: passenger.id,
          scheduleId: schedule.id,
          originStationId: IDS.stationB,
          destinationStationId: IDS.stationC,
          status: "CONFIRMED",
          totalMinor: 10000,
          displayCurrency: "ETB",
        },
      });

      const expectedBDeparture = new Date(dep.getTime() + 60 * 60_000);

      const detail: any = await bookingsService.getByRef(booking.bookingRef);
      expect(new Date(detail.schedule.departureAt).getTime()).toBe(expectedBDeparture.getTime());
      expect(detail.schedule.origin.id).toBe(IDS.stationB);
      expect(detail.schedule.destination.id).toBe(IDS.stationC);

      const list: any = await bookingsService.findByPassengerId(passenger.id);
      expect(list.items).toHaveLength(1);
      expect(new Date(list.items[0].schedule.departureAt).getTime()).toBe(expectedBDeparture.getTime());
      expect(list.items[0].schedule.originStation.id).toBe(IDS.stationB);
    });
  });
});
