import { INestApplication } from "@nestjs/common";
import { InvoiceStatus, PaymentIntentStatus, Prisma } from "@prisma/client";
import request from "supertest";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { EncryptionService } from "../src/common/crypto/encryption.service";
import { newId } from "../src/common/ids/id.util";
import { PrismaService } from "../src/prisma/prisma.service";
import { seedAdminUser, type SeededAdmin } from "./helpers/admin";
import { truncateAll } from "./helpers/db";
import { seedMerchantWithApiKey, type SeededMerchant } from "./helpers/seed";
import { buildTestApp, type TestAppHandle } from "./helpers/test-app";

const PERIOD = "2026-04";
const PERIOD_DATE = new Date(Date.UTC(2026, 3, 15));

async function seedApprovedIntent(
  prisma: PrismaService,
  merchantId: string,
  amount: string,
  createdAt: Date = PERIOD_DATE,
) {
  return prisma.paymentIntent.create({
    data: {
      id: newId("paymentIntent"),
      merchantId,
      amount: new Prisma.Decimal(amount),
      currency: "PYG",
      country: "PY",
      status: PaymentIntentStatus.approved,
      createdAt,
    },
  });
}

describe("Billing (e2e)", () => {
  let handle: TestAppHandle;
  let app: INestApplication;
  let prisma: PrismaService;
  let encryption: EncryptionService;
  let merchant: SeededMerchant;
  let admin: SeededAdmin;
  let authHeader: { Authorization: string };

  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();
    merchant = await seedMerchantWithApiKey(prisma, encryption);
    admin = await seedAdminUser(prisma);
    authHeader = { Authorization: `Bearer ${admin.accessToken}` };
  });

  describe("Pricing plans", () => {
    it("rechaza requests sin token de admin", async () => {
      await request(app.getHttpServer())
        .get(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .expect(401);
    });

    it("crea plan y al crear uno nuevo expira el anterior", async () => {
      const first = await request(app.getHttpServer())
        .post(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .send({
          name: "Plan A",
          monthlyFee: 100000,
          txFeeFixed: 500,
          txFeePercentage: 0.3,
        })
        .expect(201);
      expect(first.body.id).toMatch(/^plan_/);
      expect(first.body.active).toBe(true);

      const second = await request(app.getHttpServer())
        .post(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .send({
          name: "Plan B",
          monthlyFee: 200000,
          txFeeFixed: 600,
          txFeePercentage: 0.4,
        })
        .expect(201);

      const list = await request(app.getHttpServer())
        .get(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .expect(200);

      expect(list.body).toHaveLength(2);
      const active = list.body.find((p: { active: boolean }) => p.active);
      expect(active.id).toBe(second.body.id);

      const activeEndpoint = await request(app.getHttpServer())
        .get(`/admin/merchants/${merchant.merchantId}/pricing-plans/active`)
        .set(authHeader)
        .expect(200);
      expect(activeEndpoint.body.id).toBe(second.body.id);
    });
  });

  describe("Invoice generation", () => {
    async function createPlan(overrides: Record<string, unknown> = {}) {
      return request(app.getHttpServer())
        .post(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .send({
          name: "Plan Test",
          monthlyFee: 100000,
          txFeeFixed: 500,
          txFeePercentage: 0.3,
          ...overrides,
        })
        .expect(201);
    }

    it("genera factura con monthly_fee + tx_fee_fixed + tx_fee_percentage", async () => {
      await createPlan();
      await seedApprovedIntent(prisma, merchant.merchantId, "1000000");
      await seedApprovedIntent(prisma, merchant.merchantId, "500000");

      const res = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);

      expect(res.body.generated).toHaveLength(1);
      expect(res.body.skipped).toHaveLength(0);
      const inv = res.body.generated[0];
      expect(inv.merchantId).toBe(merchant.merchantId);
      expect(inv.period).toBe(PERIOD);
      expect(inv.txCount).toBe(2);
      expect(inv.status).toBe(InvoiceStatus.pending);

      const items = await prisma.merchantInvoiceItem.findMany({
        where: { invoiceId: inv.id },
      });
      const types = items.map((i) => i.type);
      expect(types).toContain("monthly_fee");
      expect(types).toContain("tx_fee_fixed");
      expect(types).toContain("tx_fee_percentage");

      // 1.500.000 * 0.3% = 4500
      const pct = items.find((i) => i.type === "tx_fee_percentage");
      expect(pct!.totalAmount.toString()).toBe("4500");
      // 2 tx * 500 = 1000
      const fixed = items.find((i) => i.type === "tx_fee_fixed");
      expect(fixed!.totalAmount.toString()).toBe("1000");
    });

    it("es idempotente: segunda generación skipea con razón", async () => {
      await createPlan();
      await seedApprovedIntent(prisma, merchant.merchantId, "100000");

      await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);

      const second = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);

      expect(second.body.generated).toHaveLength(0);
      expect(second.body.skipped).toHaveLength(1);
      expect(second.body.skipped[0].merchantId).toBe(merchant.merchantId);
      expect(second.body.skipped[0].reason).toMatch(/ya existe/);
    });

    it("skipea cuando el comercio no tiene plan activo", async () => {
      const res = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);

      expect(res.body.generated).toHaveLength(0);
      expect(res.body.skipped).toHaveLength(1);
      expect(res.body.skipped[0].reason).toMatch(/sin plan/);
    });

    it("rechaza period con formato inválido", async () => {
      await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ period: "2026/04" })
        .expect(400);
    });

    it("skipea cuando el plan es free", async () => {
      await createPlan({ free: true });

      const res = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);

      expect(res.body.generated).toHaveLength(0);
      expect(res.body.skipped[0].reason).toMatch(/free/);
    });

    it("yearly: skipea fuera de diciembre, factura en diciembre con monthlyFee*12", async () => {
      await createPlan({
        billingCycle: "yearly",
        monthlyFee: 50000,
        txFeeFixed: 0,
        txFeePercentage: 0,
      });

      const may = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);
      expect(may.body.skipped[0].reason).toMatch(/yearly/);

      const dec = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: "2026-12" })
        .expect(201);
      expect(dec.body.generated).toHaveLength(1);
      // 50000 * 12
      expect(dec.body.generated[0].totalAmount).toMatch(/^600000/);
    });

    it("self-billing: crea PaymentIntent en merchant interno y lo persiste en la factura", async () => {
      // El merchant seedeado por defecto actúa como merchant interno de Novasis
      // Configuramos un segundo merchant que tiene self-billing habilitado.
      const payerMerchant = await seedMerchantWithApiKey(prisma, encryption);
      process.env.SELF_BILLING_MERCHANT_ID = merchant.merchantId;

      await request(app.getHttpServer())
        .post(`/admin/merchants/${payerMerchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .send({
          name: "Plan Self-Billing",
          monthlyFee: 75000,
          txFeeFixed: 0,
          txFeePercentage: 0,
          selfBillingEnabled: true,
        })
        .expect(201);

      const res = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: payerMerchant.merchantId, period: PERIOD })
        .expect(201);

      const inv = res.body.generated[0];
      expect(inv.selfBillingIntentId).toMatch(/^pi_/);

      const intent = await prisma.paymentIntent.findUnique({
        where: { id: inv.selfBillingIntentId },
      });
      expect(intent).not.toBeNull();
      expect(intent!.merchantId).toBe(merchant.merchantId);
      expect(intent!.externalReference).toBe(inv.id);
      const meta = intent!.metadata as { kind: string; invoiceId: string };
      expect(meta.kind).toBe("self_billing");
      expect(meta.invoiceId).toBe(inv.id);

      // Simular pago aprobado del intent → la factura debe marcarse paid.
      const { InvoiceService } = await import("../src/billing/invoice.service");
      const svc = app.get(InvoiceService);
      await svc.onIntentApproved(intent!.id, intent!.metadata);

      const after = await prisma.merchantInvoice.findUnique({ where: { id: inv.id } });
      expect(after!.status).toBe("paid");
      expect(after!.paidAt).not.toBeNull();
      expect(after!.selfBillingIntentId).toBe(intent!.id);

      delete process.env.SELF_BILLING_MERCHANT_ID;
    });
  });

  describe("Invoice listing & mark-paid", () => {
    async function generateOne() {
      await request(app.getHttpServer())
        .post(`/admin/merchants/${merchant.merchantId}/pricing-plans`)
        .set(authHeader)
        .send({
          name: "Plan Test",
          monthlyFee: 50000,
          txFeeFixed: 0,
          txFeePercentage: 0,
        })
        .expect(201);
      const res = await request(app.getHttpServer())
        .post("/admin/invoices/generate")
        .set(authHeader)
        .send({ merchantId: merchant.merchantId, period: PERIOD })
        .expect(201);
      return res.body.generated[0];
    }

    it("lista facturas y filtra por period/merchantId/status", async () => {
      const inv = await generateOne();

      const all = await request(app.getHttpServer())
        .get("/admin/invoices")
        .set(authHeader)
        .expect(200);
      expect(all.body.data.length).toBeGreaterThan(0);

      const byPeriod = await request(app.getHttpServer())
        .get(`/admin/invoices?period=${PERIOD}&merchantId=${merchant.merchantId}`)
        .set(authHeader)
        .expect(200);
      expect(byPeriod.body.data).toHaveLength(1);
      expect(byPeriod.body.data[0].id).toBe(inv.id);

      const byStatus = await request(app.getHttpServer())
        .get("/admin/invoices?status=paid")
        .set(authHeader)
        .expect(200);
      expect(byStatus.body.data).toHaveLength(0);
    });

    it("mark-paid transiciona pending → paid y setea paidAt", async () => {
      const inv = await generateOne();

      const res = await request(app.getHttpServer())
        .patch(`/admin/invoices/${inv.id}/mark-paid`)
        .set(authHeader)
        .expect(200);
      expect(res.body.status).toBe(InvoiceStatus.paid);
      expect(res.body.paidAt).toBeTruthy();

      // segunda llamada → 409
      await request(app.getHttpServer())
        .patch(`/admin/invoices/${inv.id}/mark-paid`)
        .set(authHeader)
        .expect(409);
    });

    it("404 si la factura no existe", async () => {
      await request(app.getHttpServer())
        .get("/admin/invoices/inv_nonexistent")
        .set(authHeader)
        .expect(404);
    });
  });
});
