import { describe, it, expect, vi } from "vitest";
import fastify from "fastify";
import type { Pool } from "mysql2/promise";
import type Stripe from "stripe";
import { registerWebhookRoutes } from "../webhook-routes.js";

vi.mock("../../server/admin-service.js", () => ({
  recordAudit: vi.fn(),
  recordWalletChange: vi.fn(),
}));

const makeDb = () =>
  ({
    execute: async () => {
      throw new Error("DB non attendu");
    },
  }) as unknown as Pool;

/** DB avec connexion transactionnelle mockée pour les tests de webhook réels */
const makeTransactionalDb = (opts?: { insertAffectedRows?: number }) => {
  const executeFn = vi
    .fn()
    .mockResolvedValue([{ affectedRows: opts?.insertAffectedRows ?? 1 }]);
  const connectionMock = {
    execute: executeFn,
    beginTransaction: vi.fn(),
    commit: vi.fn(),
    rollback: vi.fn(),
    release: vi.fn(),
  };
  const pool = {
    execute: vi.fn().mockResolvedValue([{ affectedRows: 1 }]),
    getConnection: vi.fn(async () => connectionMock),
  };
  return { pool: pool as unknown as Pool, connectionMock, executeFn };
};

/** Mock Stripe qui retourne un event checkout.session.completed */
const makeStripeWithSession = (metadata: Record<string, string>) => {
  const stripe = {
    webhooks: {
      constructEvent: () =>
        ({
          type: "checkout.session.completed",
          data: {
            object: {
              id: "cs_test_abc123",
              metadata,
              payment_intent: "pi_test_xyz",
              amount_total: 399,
              currency: "eur",
            },
          },
        }) as unknown as Stripe.Event,
    },
  } as unknown as Stripe;
  return stripe;
};

describe("webhook-routes", () => {
  it("retourne 503 si Stripe non configuré", async () => {
    const app = fastify();
    await registerWebhookRoutes(app, makeDb(), null, undefined);

    const response = await app.inject({
      method: "POST",
      url: "/payments/stripe/webhook",
      headers: { "content-type": "application/json" },
      payload: JSON.stringify({}),
    });

    expect(response.statusCode).toBe(503);
  });

  it("retourne 400 si signature manquante", async () => {
    const app = fastify();
    const stripe = {
      webhooks: {
        constructEvent: () => {
          throw new Error("Invalid signature");
        },
      },
    } as unknown as Stripe;

    await registerWebhookRoutes(app, makeDb(), stripe, "whsec_test");

    const response = await app.inject({
      method: "POST",
      url: "/payments/stripe/webhook",
      headers: { "content-type": "application/json" },
      payload: JSON.stringify({}),
    });

    expect(response.statusCode).toBe(400);
  });

  // ────────────── Idempotence ──────────────

  describe("idempotence", () => {
    it("traite un paiement cash dans une transaction", async () => {
      const { pool, connectionMock } = makeTransactionalDb({
        insertAffectedRows: 1,
      });
      const stripe = makeStripeWithSession({
        userId: "42",
        packId: "gold",
        packType: "cash",
      });

      const app = fastify();
      await registerWebhookRoutes(app, pool, stripe, "whsec_test");

      const response = await app.inject({
        method: "POST",
        url: "/payments/stripe/webhook",
        headers: {
          "content-type": "application/json",
          "stripe-signature": "t=123,v1=abc",
        },
        payload: Buffer.from(JSON.stringify({})),
      });

      expect(response.statusCode).toBe(200);
      expect(connectionMock.beginTransaction).toHaveBeenCalled();
      expect(connectionMock.commit).toHaveBeenCalled();
      expect(connectionMock.release).toHaveBeenCalled();
      expect(connectionMock.rollback).not.toHaveBeenCalled();
    });

    it("ignore un webhook doublon (session déjà traitée)", async () => {
      // affectedRows = 0 → INSERT IGNORE n'a rien inséré → doublon
      const { pool, connectionMock } = makeTransactionalDb({
        insertAffectedRows: 0,
      });
      const stripe = makeStripeWithSession({
        userId: "42",
        packId: "gold",
        packType: "cash",
      });

      const app = fastify();
      await registerWebhookRoutes(app, pool, stripe, "whsec_test");

      const response = await app.inject({
        method: "POST",
        url: "/payments/stripe/webhook",
        headers: {
          "content-type": "application/json",
          "stripe-signature": "t=123,v1=abc",
        },
        payload: Buffer.from(JSON.stringify({})),
      });

      expect(response.statusCode).toBe(200);
      const body = JSON.parse(response.body);
      expect(body.received).toBe(true);
      // Doit rollback (pas de commit) car c'est un doublon
      expect(connectionMock.rollback).toHaveBeenCalled();
      expect(connectionMock.commit).not.toHaveBeenCalled();
      expect(connectionMock.release).toHaveBeenCalled();
    });

    it("rollback + cleanup si erreur pendant le traitement", async () => {
      const executeFn = vi
        .fn()
        // 1er appel : UPDATE pour promouvoir 'created' → 'processing'
        .mockResolvedValueOnce([{ affectedRows: 0 }])
        // 2ème appel : INSERT IGNORE → succès (nouvelle session)
        .mockResolvedValueOnce([{ affectedRows: 1 }])
        // 3ème appel : SELECT cash pack
        .mockResolvedValueOnce([
          [
            {
              pack_id: "gold",
              name: "Gold Pack",
              price: 9.99,
              hints: 100,
              undos: 50,
              replays: 25,
              bonus_points: 500,
            },
          ],
        ])
        // 4ème appel : UPDATE wallets → CRASH
        .mockRejectedValueOnce(new Error("DB timeout"));

      const connectionMock = {
        execute: executeFn,
        beginTransaction: vi.fn(),
        commit: vi.fn(),
        rollback: vi.fn(),
        release: vi.fn(),
      };
      const pool = {
        execute: vi.fn().mockResolvedValue([{ affectedRows: 1 }]),
        getConnection: vi.fn(async () => connectionMock),
      } as unknown as Pool;

      const stripe = makeStripeWithSession({
        userId: "42",
        packId: "gold",
        packType: "cash",
      });

      const app = fastify();
      await registerWebhookRoutes(app, pool, stripe, "whsec_test");

      const response = await app.inject({
        method: "POST",
        url: "/payments/stripe/webhook",
        headers: {
          "content-type": "application/json",
          "stripe-signature": "t=123,v1=abc",
        },
        payload: Buffer.from(JSON.stringify({})),
      });

      // Retourne 500 pour que Stripe réessaie automatiquement
      expect(response.statusCode).toBe(500);
      const body = JSON.parse(response.body);
      expect(body.error).toBe("Erreur traitement webhook");
      // Rollback effectué
      expect(connectionMock.rollback).toHaveBeenCalled();
      expect(connectionMock.commit).not.toHaveBeenCalled();
      expect(connectionMock.release).toHaveBeenCalled();
      // DELETE de la row 'processing' pour permettre un retry
      expect(pool.execute).toHaveBeenCalledWith(
        expect.stringContaining("DELETE FROM payment_transactions"),
        expect.arrayContaining(["cs_test_abc123"]),
      );
    });
  });
});
