/**
 * Tier-2 money-integrity suite — services behind the IAM/RabbitMQ wall, instantiated directly with
 * a real Prisma (test DB) + stubbed collaborators. Confirms critical findings:
 *   F1/F2 🔴 WalletService.topUp credits any passenger's wallet with no ownership check and no
 *            payment backing (free money).
 *   G4/G5 🔴 BookingsService.cancel computes an 80% refund but NEVER disburses it — no PaymentRefund,
 *            no wallet credit; the cancellation sits at refundStatus PENDING forever.
 *   E1/E2 🔴 ExcessBaggageService.logCharge picks the OLDEST BaggageAllowance globally, ignoring the
 *            booking's seat class, and computes fee = feePerKgMinor × excessWeightKg.
 */
import { WalletService } from "../src/modules/wallet/wallet.service";
import { BookingsService } from "../src/modules/bookings/bookings.service";
import { ExcessBaggageService } from "../src/modules/excess-baggage/excess-baggage.service";
import { getTestPrisma, disconnectTestPrisma } from "./setup/prisma";
import { truncateAllPassenger, seedCore, IDS } 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 },
  );
}

describe("Money integrity (Tier-2 direct instantiation)", () => {
  const prisma = getTestPrisma();

  beforeEach(async () => {
    await truncateAllPassenger(prisma);
    await seedCore(prisma);
  });

  afterAll(async () => {
    await disconnectTestPrisma();
  });

  // ── F1 / F2 ────────────────────────────────────────────────────────────────
  it("F1/F2 🔴 topUp credits another passenger's wallet — no ownership check, no payment backing", async () => {
    const victim = await prisma.passenger.create({ data: {} });
    await prisma.walletAccount.create({
      data: { passengerId: victim.id, balanceMinor: 0 },
    });

    const wallet = new WalletService(prisma as any);

    // An attacker-controlled call: just pass the victim's id. Nothing checks caller identity,
    // and no PaymentIntent/settlement backs the credit.
    await wallet.topUp(victim.id, 1_000_000, "free money");

    const after = await prisma.walletAccount.findUnique({
      where: { passengerId: victim.id },
    });
    expect(after?.balanceMinor).toBe(1_000_000);

    // The only ledger entry is a bare CREDIT — no linked payment.
    const ledger = await prisma.walletLedgerEntry.findMany({
      where: { walletId: after!.id },
    });
    expect(ledger).toHaveLength(1);
    expect(ledger[0].type).toBe("CREDIT");
    expect(ledger[0].relatedBookingId ?? null).toBeNull();
  });

  // ── G4 / G5 ────────────────────────────────────────────────────────────────
  it("G4/G5 🔴 cancel() computes floor(total*0.8) refund but never disburses it (stuck PENDING)", async () => {
    const passenger = await prisma.passenger.create({ data: {} });
    // Give the passenger a wallet so we can prove NO refund lands in it.
    const w = await prisma.walletAccount.create({
      data: { passengerId: passenger.id, balanceMinor: 0 },
    });
    const schedule = await makeSchedule(prisma, passenger.id);

    const booking = await prisma.booking.create({
      data: {
        bookingRef: "CXL-0001",
        passengerId: passenger.id,
        scheduleId: schedule.id,
        totalMinor: 30_000,
        displayCurrency: "ETB",
        status: "CONFIRMED",
      },
    });

    const bookings = new BookingsService(
      prisma as any,
      asyncStub(), // dataSource
      asyncStub(), // seatsService
      asyncStub(), // ticketsService — constructor gained this param since this test was written
      { emit: () => true } as any, // eventEmitter
      asyncStub(), // verifaydaService
      asyncStub(), // currencyService
      asyncStub(), // fareEngine
      asyncStub(), // auditService
    );

    const result: any = await bookings.cancel(booking.bookingRef, "test");

    // Refund is COMPUTED as 80%:
    expect(result.refundAmount).toBe(Math.floor(30_000 * 0.8) / 100); // 240.00

    // …but recorded only as PENDING, and never actually paid out:
    const cancellation = await prisma.bookingCancellation.findFirst({
      where: { bookingId: booking.id },
    });
    expect(cancellation?.refundStatus).toBe("PENDING");

    // No PaymentRefund row was created anywhere (isolated DB) and the wallet was NOT credited.
    const refundCount = await prisma.paymentRefund.count();
    expect(refundCount).toBe(0);
    const walletAfter = await prisma.walletAccount.findUnique({ where: { id: w.id } });
    expect(walletAfter?.balanceMinor).toBe(0);
  });

  // ── E1 / E2 ────────────────────────────────────────────────────────────────
  it("E1/E2 🔴 excess-baggage uses the OLDEST allowance globally (ignores seat class); fee = rate×kg", async () => {
    const passenger = await prisma.passenger.create({ data: {} });
    const schedule = await makeSchedule(prisma, passenger.id);
    const booking = await prisma.booking.create({
      data: {
        bookingRef: "BAG-0001",
        passengerId: passenger.id,
        scheduleId: schedule.id,
        totalMinor: 30_000,
        status: "CONFIRMED",
      },
    });

    // Oldest allowance is for the LOCAL class (rate 50). A later one for INTL (rate 200) should win
    // for an intl booking — but logCharge ignores seat class and takes the oldest row.
    await prisma.baggageAllowance.create({
      data: { seatClassId: IDS.seatClassLocal, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 50 },
    });
    await prisma.baggageAllowance.create({
      data: { seatClassId: IDS.seatClassIntl, maxWeightKg: 20, maxPiecesCount: 2, excessFeePerKg: 200 },
    });

    const service = new ExcessBaggageService(
      prisma as any,
      asyncStub(), // auditService
      asyncStub(), // paymentClient
      asyncStub(), // notifications
      asyncStub(), // smsClient
      asyncStub(), // emailClient
    );

    const charge: any = await service.logCharge({
      bookingId: booking.id,
      excessWeightKg: 10,
      collectCash: true,
    } as any);

    // Used the oldest (LOCAL, 50) not any seat-class-matched rate; fee = 50 × 10.
    expect(charge.feePerKgMinor).toBe(50);
    expect(charge.totalMinor).toBe(50 * 10);
  });
});

let trainSeq = 0;

/** Minimal TrainSchedule (+train) so booking/cancel fixtures satisfy FKs. */
async function makeSchedule(prisma: any, _passengerId: string) {
  const train = await prisma.train.create({
    data: { number: `T-${++trainSeq}`, name: "Test Train" },
  });
  return prisma.trainSchedule.create({
    data: {
      trainId: train.id,
      routeId: IDS.route,
      originStationId: IDS.stationA,
      destinationStationId: IDS.stationB,
      departureAt: new Date(Date.now() + 86_400_000),
      arrivalAt: new Date(Date.now() + 90_000_000),
      durationMinutes: 60,
    },
  });
}
