/**
 * Ticketing/boarding coverage — TicketsService.generate()/scanAndBoard()/validate() and the
 * smart-seat-reassignment fallback, none of which had any e2e coverage before this suite
 * (colocated tickets.service.spec.ts only covers offline batch validation, a different path).
 *
 * IMPORTANT: scanAndBoard() catches every internal error and returns
 * `{ success: false, error, errorCode }` rather than throwing — assertions against it check
 * the return value, not `.rejects.toThrow()`.
 *
 * Same Tier-2 direct-instantiation pattern as reserve-seat-issue-booking.e2e-spec.ts /
 * booking-types.e2e-spec.ts.
 */
import { IdDocumentType, PaymentIntentStatus, PaymentMethodType } from "@prisma/client";
import { SchedulesService } from "../src/modules/schedules/schedules.service";
import { SeatsService } from "../src/modules/seats/seats.service";
import { TicketsService } from "../src/modules/tickets/tickets.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, resetAndSeedCore } from "./fixtures/seed-core";

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

describe("Ticketing — generate / scanAndBoard / validate / smart-reassign", () => {
  let harness: ServiceHarness;
  let schedulesService: SchedulesService;
  let seatsService: SeatsService;
  let ticketsService: TicketsService;
  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 systemConfig = new SystemConfigService(harness.prisma as any);

    seatsService = new SeatsService(harness.prisma as any, asyncStub(), systemConfig, asyncStub(), asyncStub());
    ticketsService = new TicketsService(harness.prisma as any, asyncStub(), systemConfig, asyncStub(), asyncStub());
    guestBookingService = new GuestBookingService(
      harness.prisma as any,
      seatsService,
      asyncStub(),
      currencyService,
      asyncStub(),
      fareEngine,
      { emit: () => true } as any,
      asyncStub(), // paymentsService — not used; tests drive generate()/scanAndBoard() directly
      asyncStub(),
      asyncStub(),
    );
  });

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

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

  async function createTestSchedule(opts: { trainNumber: string; departureAt: Date; arrivalAt: Date; seatCount?: number }) {
    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: opts.seatCount ?? 4, sequence: 1, status: "ACTIVE" },
    });
    const seatCount = opts.seatCount ?? 4;
    const seatNumbers = Array.from({ length: seatCount }, (_, i) => `1${String.fromCharCode(65 + i)}`);
    const seats = await Promise.all(
      seatNumbers.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 passengerDto(seatId: string, overrides: Partial<Record<string, any>> = {}) {
    return {
      seatId,
      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);

  /** Creates a real PENDING_PAYMENT ONE_WAY booking via the guest flow. */
  async function createOneWayBooking(scheduleId: string, seatId: string, originStationId = IDS.stationA, destinationStationId = IDS.stationB) {
    const hold = await seatsService.holdSeats({
      scheduleId,
      originStationId,
      destinationStationId,
      passengers: [{ passengerId: "55555555-5555-4555-8555-500000000001", seatId }],
    } as any);
    return guestBookingService.createGuestBooking({
      scheduleId,
      holdId: (hold as any).holdId,
      originStationId,
      destinationStationId,
      seatClassId: IDS.seatClassLocal,
      passengers: [passengerDto(seatId)],
    } as any) as Promise<any>;
  }

  async function markSucceeded(bookingId: string) {
    await harness.prisma.paymentIntent.create({
      data: { bookingId, amountMinor: 30_000, currency: "ETB", method: PaymentMethodType.CARD, status: PaymentIntentStatus.SUCCEEDED, paidAt: new Date() },
    });
    await harness.prisma.booking.update({ where: { id: bookingId }, data: { status: "CONFIRMED" } });
  }

  describe("generate()", () => {
    it("issues a ticket for a CONFIRMED booking with a SUCCEEDED PaymentIntent, and marks the seat BOOKED", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-GEN-${Date.now()}`, departureAt: future(180), arrivalAt: future(220) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(booking.id);

      const result = await ticketsService.generate(booking.id);
      expect(result.totalTickets).toBe(1);

      const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
      expect(ticket).toBeTruthy();
      expect(ticket?.qrPayload).toBeTruthy();
      expect(ticket?.barcodePayload).toBeTruthy();

      const seat = await harness.prisma.seat.findUnique({ where: { id: seats[0].id } });
      expect(seat?.status).toBe("BOOKED");
    });

    it("rejects with 'Payment not completed' when there's no PaymentIntent at all", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-NOPI-${Date.now()}`, departureAt: future(180), arrivalAt: future(220) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);

      await expect(ticketsService.generate(booking.id)).rejects.toThrow(/Payment not completed/i);
    });

    it("rejects with 'Payment not completed' when the PaymentIntent hasn't succeeded", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-PENDING-${Date.now()}`, departureAt: future(180), arrivalAt: future(220) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await harness.prisma.paymentIntent.create({
        data: { bookingId: booking.id, amountMinor: 30_000, currency: "ETB", method: PaymentMethodType.CARD, status: PaymentIntentStatus.PROCESSING },
      });

      await expect(ticketsService.generate(booking.id)).rejects.toThrow(/Payment not completed/i);
    });

    it("auto-confirms a PENDING_PAYMENT booking whose PaymentIntent already SUCCEEDED (missed webhook)", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-HEAL-${Date.now()}`, departureAt: future(180), arrivalAt: future(220) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      expect(booking.status).toBe("PENDING_PAYMENT");
      await harness.prisma.paymentIntent.create({
        data: { bookingId: booking.id, amountMinor: 30_000, currency: "ETB", method: PaymentMethodType.CARD, status: PaymentIntentStatus.SUCCEEDED, paidAt: new Date() },
      });

      await ticketsService.generate(booking.id);

      const refreshed = await harness.prisma.booking.findUnique({ where: { id: booking.id } });
      expect(refreshed?.status).toBe("CONFIRMED");
    });
  });

  describe("smart seat reassignment", () => {
    it("generate() throws ConflictException on a genuinely overlapping confirmed seat; smartAssignAndGenerate() recovers onto a free seat", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-CONFLICT-${Date.now()}`, departureAt: future(180), arrivalAt: future(220), seatCount: 3 });

      // Booking A already CONFIRMED on seats[0], full A->B route (matches the schedule's own
      // origin/destination, guaranteeing segment overlap with anything else on seats[0]).
      const passengerA = await harness.prisma.passenger.create({ data: {} });
      const bookingA = await harness.prisma.booking.create({
        data: {
          bookingRef: `CONFA${Date.now()}`, passengerId: passengerA.id, scheduleId: schedule.id,
          originStationId: IDS.stationA, destinationStationId: IDS.stationB,
          status: "CONFIRMED", totalMinor: 30_000, displayCurrency: "ETB",
          seats: { create: [{ seatId: seats[0].id, scheduleId: schedule.id, passengerName: "Passenger A", passengerCategory: "ADULT", fareMinor: 30_000, displayCurrency: "ETB" }] },
        },
      });
      void bookingA;

      // Booking B independently references the SAME seat (simulating however the conflict
      // arose — the point of this test is the recovery path, not the cause).
      const bookingB = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(bookingB.id);

      await expect(ticketsService.generate(bookingB.id)).rejects.toThrow(/already confirmed for another booking/i);

      const recovered = await ticketsService.smartAssignAndGenerate(bookingB.id);
      expect(recovered.totalTickets).toBe(1);

      const bookingSeat = await harness.prisma.bookingSeat.findFirst({ where: { bookingId: bookingB.id } });
      expect(bookingSeat?.seatId).not.toBe(seats[0].id); // reassigned off the conflicting seat
      expect([seats[1].id, seats[2].id]).toContain(bookingSeat?.seatId);

      const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: bookingB.id } });
      expect(ticket?.seatId).toBe(bookingSeat?.seatId);
    });
  });

  describe("scanAndBoard()", () => {
    it("boards successfully within the boarding window", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-BOARD-OK-${Date.now()}`, departureAt: future(60), arrivalAt: future(120) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(booking.id);
      await ticketsService.generate(booking.id);

      const result = await ticketsService.scanAndBoard(booking.bookingRef, "gate-validator-1");
      expect(result.success).toBe(true);
      expect(result.boarding?.seat).toBe(seats[0].seatNumber);

      const ticket = await harness.prisma.ticket.findFirst({ where: { bookingId: booking.id } });
      expect(ticket?.validatedAt).toBeTruthy();
    });

    it("refuses boarding before the boarding window opens", async () => {
      // Default boarding window is 4h (SystemConfigService DEFAULTS) — 6h out is still closed.
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-BOARD-EARLY-${Date.now()}`, departureAt: future(360), arrivalAt: future(420) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(booking.id);
      await ticketsService.generate(booking.id);

      const result = await ticketsService.scanAndBoard(booking.bookingRef, "gate-validator-1");
      expect(result.success).toBe(false);
      expect(result.error).toMatch(/Boarding opens \d+ hour\(s\) before departure/i);
    });

    it("refuses boarding once departure has passed", async () => {
      // Book while departure is comfortably in the future (holdSeats itself refuses a hold
      // within 30min of departure), THEN move departure into the past — both the schedule's
      // own departureAt AND the origin stop's plannedDepartureAt, since scanAndBoard resolves
      // the boarding time via resolveBookingSegment(), which prefers the TripStopTime over
      // the raw schedule field.
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-BOARD-LATE-${Date.now()}`, departureAt: future(180), arrivalAt: future(240) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(booking.id);
      await ticketsService.generate(booking.id);

      const past = new Date(Date.now() - 60_000);
      await harness.prisma.trainSchedule.update({ where: { id: schedule.id }, data: { departureAt: past } });
      await harness.prisma.tripStopTime.updateMany({ where: { scheduleId: schedule.id, stationId: IDS.stationA }, data: { plannedDepartureAt: past } });

      const result = await ticketsService.scanAndBoard(booking.bookingRef, "gate-validator-1");
      expect(result.success).toBe(false);
      expect(result.error).toMatch(/Boarding is closed/i);
    });
  });

  describe("validate()", () => {
    it("ONE_WAY: a second validate() call on the same ticket is idempotent, not an error", async () => {
      const { schedule, seats } = await createTestSchedule({ trainNumber: `TIX-VAL-ONEWAY-${Date.now()}`, departureAt: future(60), arrivalAt: future(120) });
      const booking = await createOneWayBooking(schedule.id, seats[0].id);
      await markSucceeded(booking.id);
      await ticketsService.generate(booking.id);

      const first = await ticketsService.validate(booking.bookingRef, "gate-validator-1");
      expect(first.validated).toBe(true);
      expect((first as any).alreadyValidated).toBeUndefined();

      const second = await ticketsService.validate(booking.bookingRef, "gate-validator-1");
      expect(second.validated).toBe(true);
      expect((second as any).alreadyValidated).toBe(true);
    });
  });
});
