import { INestApplication } from "@nestjs/common";
import request from "supertest";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { EncryptionService } from "../src/common/crypto/encryption.service";
import { PrismaService } from "../src/prisma/prisma.service";
import { truncateAll } from "./helpers/db";
import { seedMerchantWithApiKey, type SeededMerchant } from "./helpers/seed";
import { buildTestApp, type TestAppHandle } from "./helpers/test-app";

async function createSessionForIntent(
  app: INestApplication,
  bearer: string,
): Promise<{ intentId: string; publicToken: string; sessionId: string }> {
  const intent = await request(app.getHttpServer())
    .post("/payment-intents")
    .set("Authorization", `Bearer ${bearer}`)
    .send({ amount: 150_000, currency: "PYG", country: "PY", description: "Order #1" })
    .expect(201);

  const session = await request(app.getHttpServer())
    .post("/checkout-sessions")
    .set("Authorization", `Bearer ${bearer}`)
    .send({ intentId: intent.body.id })
    .expect(201);

  return {
    intentId: intent.body.id,
    publicToken: session.body.publicToken,
    sessionId: session.body.id,
  };
}

describe("Public Checkout Session (e2e)", () => {
  let handle: TestAppHandle;
  let app: INestApplication;
  let prisma: PrismaService;
  let encryption: EncryptionService;
  let seeded: SeededMerchant;

  beforeAll(async () => {
    handle = await buildTestApp();
    app = handle.app;
    prisma = app.get(PrismaService);
    encryption = app.get(EncryptionService);
  });

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

  beforeEach(async () => {
    await truncateAll(prisma);
    handle.fakeRouter.reset();
    seeded = await seedMerchantWithApiKey(prisma, encryption);
    const cfg = await prisma.merchantProviderConfig.findUniqueOrThrow({
      where: { id: seeded.providerConfigId },
    });
    handle.fakeRouter.registerConfig(cfg);
  });

  describe("GET /public/checkout-sessions/:publicToken", () => {
    it("returns safe info for valid token", async () => {
      const { publicToken } = await createSessionForIntent(app, seeded.apiKey);
      const res = await request(app.getHttpServer())
        .get(`/public/checkout-sessions/${publicToken}`)
        .expect(200);

      expect(res.body.amount).toBe("150000");
      expect(res.body.currency).toBe("PYG");
      expect(res.body.merchantName).toBe("Test Merchant");
      // Sensible: nunca debe filtrar IDs internos del merchant ni el clientSecret.
      expect(res.body.merchantId).toBeUndefined();
      expect(res.body.clientSecret).toBeUndefined();
      expect(res.body.intentId).toBeUndefined();
    });

    it("returns 404 for unknown token (uniform anti-enumeration)", async () => {
      await request(app.getHttpServer())
        .get(`/public/checkout-sessions/does-not-exist`)
        .expect(404);
    });

    it("returns 404 if merchant is not active", async () => {
      const { publicToken } = await createSessionForIntent(app, seeded.apiKey);
      await prisma.merchant.update({
        where: { id: seeded.merchantId },
        data: { status: "suspended" },
      });
      await request(app.getHttpServer())
        .get(`/public/checkout-sessions/${publicToken}`)
        .expect(404);
    });
  });

  describe("POST /public/checkout-sessions/:publicToken/confirm", () => {
    it("confirms with provider and returns approved status + redirectUrl", async () => {
      handle.fakeRouter.program({ createStatus: "approved", redirectUrl: "https://prov.test/r/abc" });
      const { publicToken, intentId } = await createSessionForIntent(app, seeded.apiKey);

      const res = await request(app.getHttpServer())
        .post(`/public/checkout-sessions/${publicToken}/confirm`)
        .send({ customer: { email: "buyer@example.com", name: "Buyer" } })
        .expect(201);

      expect(res.body.status).toBe("approved");
      expect(res.body.redirectUrl).toBe("https://prov.test/r/abc");

      const intent = await prisma.paymentIntent.findUniqueOrThrow({ where: { id: intentId } });
      expect(intent.status).toBe("approved");
      expect(intent.customerId).not.toBeNull();
    });

    it("locks session after 5 failed attempts", async () => {
      handle.fakeRouter.program({ createStatus: "rejected" });
      const { publicToken, sessionId } = await createSessionForIntent(app, seeded.apiKey);

      for (let i = 0; i < 5; i++) {
        await request(app.getHttpServer())
          .post(`/public/checkout-sessions/${publicToken}/confirm`)
          .send({})
          .expect((r) => {
            if (r.status !== 201 && r.status !== 400) {
              throw new Error(`unexpected status ${r.status}`);
            }
          });
      }

      const locked = await prisma.checkoutSession.findUniqueOrThrow({ where: { id: sessionId } });
      expect(locked.failedAttempts).toBeGreaterThanOrEqual(5);

      await request(app.getHttpServer())
        .post(`/public/checkout-sessions/${publicToken}/confirm`)
        .send({})
        .expect(404);

      await request(app.getHttpServer())
        .get(`/public/checkout-sessions/${publicToken}`)
        .expect(404);
    });

    it("rejects invalid email payloads", async () => {
      const { publicToken } = await createSessionForIntent(app, seeded.apiKey);
      await request(app.getHttpServer())
        .post(`/public/checkout-sessions/${publicToken}/confirm`)
        .send({ customer: { email: "not-an-email" } })
        .expect(400);
    });

    it("returns 404 (not 410) for expired sessions to avoid leaking validity", async () => {
      const { publicToken, sessionId } = await createSessionForIntent(app, seeded.apiKey);
      await prisma.checkoutSession.update({
        where: { id: sessionId },
        data: { expiresAt: new Date(Date.now() - 1000) },
      });
      // Primer GET marca como expired
      await request(app.getHttpServer())
        .get(`/public/checkout-sessions/${publicToken}`)
        .expect(200);
      // Confirm sobre expired → 404 (no abre flow nuevo)
      await request(app.getHttpServer())
        .post(`/public/checkout-sessions/${publicToken}/confirm`)
        .send({})
        .expect(404);
    });
  });

  describe("Security headers", () => {
    it("applies frame-deny + no-referrer on public GET", async () => {
      const { publicToken } = await createSessionForIntent(app, seeded.apiKey);
      const res = await request(app.getHttpServer())
        .get(`/public/checkout-sessions/${publicToken}`)
        .expect(200);

      expect(res.headers["x-frame-options"]).toBe("DENY");
      expect(res.headers["referrer-policy"]).toBe("no-referrer");
    });
  });
});
