import { jest } from "@jest/globals";
import request from "supertest";
import jwt from "jsonwebtoken";
import bcrypt from "bcryptjs";
import app from "../../src/app.js";
import db from "../../src/models/index.js";
import PsbService from "../../src/thirdParties/psbService.js";
import WebhookService from "../../src/services/webhookService.js";
import { setupTestDb, cleanUpTestDb } from "../helpers/dbSetup.js";

describe("API Routes (Integration Tests)", () => {
  let adminToken;
  let superAdminToken;
  let testMerchant;
  let merchantToken;
  let customerId;
  let rawMerchantPrivateKey = "sk_testmerchantprivatekey";

  beforeAll(async () => {
    await setupTestDb();

    // Mock PSB Static Account creation
    jest.spyOn(PsbService, "createStaticAccount").mockResolvedValue({
      customer: { account: { number: "6400000001" } },
    });

    // Mock Webhook Service delivery
    jest.spyOn(WebhookService, "notifyMerchant").mockResolvedValue(true);
    jest.spyOn(WebhookService, "executeDelivery").mockResolvedValue(true);

    // 1. Seed admin and superadmin users
    const hashedPassword = await bcrypt.hash("password123", 10);
    
    const admin = await db.Admin.create({
      fullName: "Platform Admin",
      email: "admin@airpero.com",
      password: hashedPassword,
      role: "admin",
      status: "active",
    });

    const superAdmin = await db.Admin.create({
      fullName: "Platform SuperAdmin",
      email: "superadmin@airpero.com",
      password: hashedPassword,
      role: "superadmin",
      status: "active",
    });

    adminToken = jwt.sign(
      { id: admin.id, role: "admin" },
      process.env.JWT_SECRET || "airpero_super_secret_key"
    );

    superAdminToken = jwt.sign(
      { id: superAdmin.id, role: "superadmin" },
      process.env.JWT_SECRET || "airpero_super_secret_key"
    );

    // 2. Create a test merchant
    const hashedMerchantPrivateKey = await bcrypt.hash(rawMerchantPrivateKey, 10);
    testMerchant = await db.Merchant.create({
      businessName: "Test Merchant Co",
      email: "info@testmerchant.com",
      password: hashedPassword,
      publicKey: "pk_testmerchantpublickey",
      privateKey: hashedMerchantPrivateKey,
      merchantAccountNumber: "6400001111",
      balance: 50000.0, // Pre-funded wallet
      status: "active",
    });

    merchantToken = jwt.sign(
      { id: testMerchant.id, role: "merchant" },
      process.env.JWT_SECRET || "airpero_super_secret_key"
    );
  });

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

  describe("Admin Auth & Exclusive Routes", () => {
    it("should allow admin login and return token", async () => {
      const res = await request(app)
        .post("/api/v1/admin/login")
        .send({ email: "admin@airpero.com", password: "password123" });

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body.token).toBeDefined();
    });

    it("should block non-superadmin from editing fee settings", async () => {
      const res = await request(app)
        .patch("/api/v1/admin/settings")
        .set("Authorization", `Bearer ${adminToken}`)
        .send({ depositFeePercent: 1.2 });

      expect(res.status).toBe(403);
      expect(res.body.success).toBe(false);
    });

    it("should allow superadmin to update fee settings", async () => {
      const res = await request(app)
        .patch("/api/v1/admin/settings")
        .set("Authorization", `Bearer ${superAdminToken}`)
        .send({ depositFeePercent: 0.8 });

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
    });
  });

  describe("Merchant Operations", () => {
    it("should self-register a new merchant and allocate a static account", async () => {
      const res = await request(app)
        .post("/api/v1/merchants/register")
        .send({
          businessName: "Self Registered Merchant",
          email: "selfreg@merchant.com",
          password: "securepassword",
        });

      expect(res.status).toBe(201);
      expect(res.body.success).toBe(true);
      expect(res.body.data.merchantAccountNumber).toBe("6400000001");
    });

    it("should fetch merchant profile using JWT token", async () => {
      const res = await request(app)
        .get("/api/v1/merchants/me")
        .set("Authorization", `Bearer ${merchantToken}`);

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);
      expect(res.body.data.businessName).toBe("Test Merchant Co");
    });
  });

  describe("Customer Management & API Authentication", () => {
    it("should block customer creation without API keys", async () => {
      const res = await request(app)
        .post("/api/v1/customers")
        .send({ fullName: "Anonymous Customer", email: "anon@cust.com" });

      expect(res.status).toBe(401);
    });

    it("should create a customer when valid merchant API keys are provided", async () => {
      const res = await request(app)
        .post("/api/v1/customers")
        .set("x-public-key", "pk_testmerchantpublickey")
        .set("x-private-key", rawMerchantPrivateKey)
        .send({
          fullName: "Alice Cooper",
          email: "alice@cooper.com",
          phoneNumber: "+2348012345678",
        });

      expect(res.status).toBe(201);
      expect(res.body.success).toBe(true);
      expect(res.body.data.accountNumber).toBe("6400000001");
      customerId = res.body.data.id;
    });

    it("should manually credit a customer from the merchant balance", async () => {
      const mBefore = await db.Merchant.findByPk(testMerchant.id);
      console.log("DEBUG: merchant balance before credit request is", mBefore.balance);

      const res = await request(app)
        .post(`/api/v1/customers/${customerId}/credit`)
        .set("Authorization", `Bearer ${merchantToken}`)
        .send({ amount: 1000.0, narration: "Testing credit flow" });

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);

      const customer = await db.Customer.findByPk(customerId);
      expect(parseFloat(customer.balance)).toBe(1000.00);

      const merchant = await db.Merchant.findByPk(testMerchant.id);
      expect(parseFloat(merchant.balance)).toBe(49000.00);
    });

    it("should manually debit a customer and return funds to merchant balance", async () => {
      const res = await request(app)
        .post(`/api/v1/customers/${customerId}/debit`)
        .set("Authorization", `Bearer ${merchantToken}`)
        .send({ amount: 400.0, reason: "Direct debit recovery" });

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);

      const customer = await db.Customer.findByPk(customerId);
      expect(parseFloat(customer.balance)).toBe(600.00);

      const merchant = await db.Merchant.findByPk(testMerchant.id);
      expect(parseFloat(merchant.balance)).toBe(49400.00); // 49000 + 400
    });
  });

  describe("9PSB Webhook Handlers (Pay-in Flow)", () => {
    it("should process customer deposit, deduct fee from merchant, and deduplicate", async () => {
      // Create a customer with a known static virtual account
      const customer = await db.Customer.create({
        merchantId: testMerchant.id,
        fullName: "Bob Marley",
        email: "bob@marley.com",
        accountNumber: "6400009999",
      });

      const sessionid = `SESSION-WEBHOOK-${Date.now()}`;
      const payload = {
        transaction: {
          sessionid,
          date: "2026-06-24T14:00:00",
        },
        customer: {
          account: {
            number: "6400009999",
            sendername: "Rita Marley",
            senderaccountnumber: "1100223344",
            senderbankname: "Wema Bank",
            senderbankcode: "057",
          },
        },
        order: {
          amount: "5000", // Deposit amount
          currency: "NGN",
        },
        code: "00",
      };

      // 1. Initial Deposit
      const res = await request(app)
        .post("/api/v1/webhooks/9psb")
        .send(payload);

      expect(res.status).toBe(200);
      expect(res.body.success).toBe(true);

      // Verify customer credited with 5000 NGN
      const updatedCustomer = await db.Customer.findByPk(customer.id);
      expect(parseFloat(updatedCustomer.balance)).toBe(5000.00);

      // Verify merchant charged 0.8% fee on deposit (updated from 0.5% to 0.8% in settings test: 0.8% of 5000 = 40 NGN)
      // Original merchant balance was 49400.00, should be 49400 - 40 = 49360.00
      const updatedMerchant = await db.Merchant.findByPk(testMerchant.id);
      expect(parseFloat(updatedMerchant.balance)).toBe(49360.00);

      // Verify system profit generated (40 NGN)
      const profit = await db.SystemProfit.findOne({ where: { transactionId: sessionid } });
      expect(parseFloat(profit.amount)).toBe(40.00);

      // 2. Duplicate Deposit Attempt (Deduplication)
      const resDuplicate = await request(app)
        .post("/api/v1/webhooks/9psb")
        .send(payload);

      expect(resDuplicate.status).toBe(200);

      // Verify customer balance was NOT credited again
      const postDuplicateCustomer = await db.Customer.findByPk(customer.id);
      expect(parseFloat(postDuplicateCustomer.balance)).toBe(5000.00);
    });
  });

  describe("API Security & Tenant Isolation", () => {
    let merchantBToken;
    let merchantB;

    beforeAll(async () => {
      // Register Merchant B
      const res = await request(app)
        .post("/api/v1/merchants/register")
        .send({
          businessName: "Merchant B Inc",
          email: `merchantb_${Date.now()}@example.com`,
          password: "password123",
          preview: "MB"
        });
      
      expect(res.status).toBe(201);
      merchantB = res.body.data;

      // Log in as Merchant B to get JWT
      const loginRes = await request(app)
        .post("/api/v1/merchants/login")
        .send({
          email: res.body.data.email,
          password: "password123"
        });
      
      expect(loginRes.status).toBe(200);
      merchantBToken = loginRes.body.token;
    });

    it("should prevent Merchant B from accessing Merchant A's customer details", async () => {
      const res = await request(app)
        .get(`/api/v1/customers/${customerId}`)
        .set("Authorization", `Bearer ${merchantBToken}`);

      expect(res.status).toBe(404);
      expect(res.body.success).toBe(false);
    });

    it("should prevent Merchant B from accessing Merchant A's customer balance", async () => {
      const res = await request(app)
        .get(`/api/v1/customers/${customerId}/balance`)
        .set("Authorization", `Bearer ${merchantBToken}`);

      expect(res.status).toBe(404);
      expect(res.body.success).toBe(false);
    });

    it("should prevent Merchant B from accessing Merchant A's customer report", async () => {
      const res = await request(app)
        .get(`/api/v1/customers/${customerId}/report`)
        .set("Authorization", `Bearer ${merchantBToken}`);

      expect(res.status).toBe(404);
      expect(res.body.success).toBe(false);
    });

    it("should prevent Merchant B from requerying Merchant A's transaction", async () => {
      // Create a dummy transaction for Merchant A
      const tx = await db.Transaction.create({
        merchantId: testMerchant.id,
        transaction_id: `TX-ISOLATION-${Date.now()}`,
        type: "credit",
        class: "transfer",
        amount: 100,
        total_amount: 100,
        status: "success",
      });

      const res = await request(app)
        .post("/api/v1/transactions/requery")
        .set("Authorization", `Bearer ${merchantBToken}`)
        .send({ transactionId: tx.transaction_id });

      expect(res.status).toBe(404);
      expect(res.body.success).toBe(false);
    });

    it("should enforce rate limiting on login after exceeding 5 attempts", async () => {
      // We perform 5 failed logins
      for (let i = 0; i < 5; i++) {
        await request(app)
          .post("/api/v1/merchants/login")
          .send({ email: "bad-login@attempt.com", password: "wrongpassword" });
      }

      // The 6th attempt should return 429
      const res = await request(app)
        .post("/api/v1/merchants/login")
        .send({ email: "bad-login@attempt.com", password: "wrongpassword" });

      expect(res.status).toBe(429);
      expect(res.body.message).toContain("Too many login attempts");
    });
  });

  describe("Merchant IP Whitelisting & SuperAdmin Protection", () => {
    let whitelistEntryId;

    it("should allow a merchant to manage (add, list, delete) their IP whitelist", async () => {
      // 1. Add IP to whitelist
      const addRes = await request(app)
        .post("/api/v1/merchants/ip-whitelist")
        .set("Authorization", `Bearer ${merchantToken}`)
        .send({ ipAddress: "192.168.1.100" });

      expect(addRes.status).toBe(201);
      expect(addRes.body.success).toBe(true);
      expect(addRes.body.data.ipAddress).toBe("192.168.1.100");
      whitelistEntryId = addRes.body.data.id;

      // 2. Try to add same IP again (should return 400). Request must come from the whitelisted IP now.
      const addDupRes = await request(app)
        .post("/api/v1/merchants/ip-whitelist")
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "192.168.1.100")
        .send({ ipAddress: "192.168.1.100" });

      expect(addDupRes.status).toBe(400);
      expect(addDupRes.body.success).toBe(false);

      // 3. List IP whitelist
      const listRes = await request(app)
        .get("/api/v1/merchants/ip-whitelist")
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "192.168.1.100");

      expect(listRes.status).toBe(200);
      expect(listRes.body.success).toBe(true);
      expect(listRes.body.data.some(entry => entry.ipAddress === "192.168.1.100")).toBe(true);

      // 4. Delete IP from whitelist
      const deleteRes = await request(app)
        .delete(`/api/v1/merchants/ip-whitelist/${whitelistEntryId}`)
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "192.168.1.100");

      expect(deleteRes.status).toBe(200);
      expect(deleteRes.body.success).toBe(true);

      // 5. Verify deleted from list
      const listAfterRes = await request(app)
        .get("/api/v1/merchants/ip-whitelist")
        .set("Authorization", `Bearer ${merchantToken}`); // Since whitelist is now empty, localhost is allowed

      expect(listAfterRes.body.data.some(entry => entry.id === whitelistEntryId)).toBe(false);
    });

    it("should restrict merchant API request if requesting IP is not whitelisted, and allow if whitelisted", async () => {
      // 1. Add IP to whitelist
      const addRes = await request(app)
        .post("/api/v1/merchants/ip-whitelist")
        .set("Authorization", `Bearer ${merchantToken}`)
        .send({ ipAddress: "198.51.100.5" });

      expect(addRes.status).toBe(201);
      const whitelistId = addRes.body.data.id;

      // 2. Test API Key Auth: request from unwhitelisted IP
      const apiKeyDenyRes = await request(app)
        .post("/api/v1/customers")
        .set("x-public-key", "pk_testmerchantpublickey")
        .set("x-private-key", rawMerchantPrivateKey)
        .set("X-Forwarded-For", "203.0.113.50")
        .send({
          fullName: "Should Block",
          email: "shouldblock@cust.com",
        });

      expect(apiKeyDenyRes.status).toBe(403);
      expect(apiKeyDenyRes.body.message).toContain("IP Address not whitelisted");

      // 3. Test API Key Auth: request from whitelisted IP
      const apiKeyAllowRes = await request(app)
        .post("/api/v1/customers")
        .set("x-public-key", "pk_testmerchantpublickey")
        .set("x-private-key", rawMerchantPrivateKey)
        .set("X-Forwarded-For", "198.51.100.5")
        .send({
          fullName: "Charlie Brown",
          email: "charlie@brown.com",
          phoneNumber: "+2348000000001",
        });

      expect(apiKeyAllowRes.status).toBe(201);
      expect(apiKeyAllowRes.body.success).toBe(true);

      // 4. Test JWT Auth: request from unwhitelisted IP
      const jwtDenyRes = await request(app)
        .get("/api/v1/merchants/me")
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "203.0.113.50");

      expect(jwtDenyRes.status).toBe(403);
      expect(jwtDenyRes.body.message).toContain("IP Address not whitelisted");

      // 5. Test JWT Auth: request from whitelisted IP
      const jwtAllowRes = await request(app)
        .get("/api/v1/merchants/me")
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "198.51.100.5");

      expect(jwtAllowRes.status).toBe(200);
      expect(jwtAllowRes.body.success).toBe(true);

      // Clean up IP whitelist entry (must delete from a whitelisted IP)
      const cleanupRes = await request(app)
        .delete(`/api/v1/merchants/ip-whitelist/${whitelistId}`)
        .set("Authorization", `Bearer ${merchantToken}`)
        .set("X-Forwarded-For", "198.51.100.5");

      expect(cleanupRes.status).toBe(200);
    });

    it("should restrict SuperAdmin actions if the IP is not in the allowed list", async () => {
      const originalAllowedIps = process.env.SUPERADMIN_ALLOWED_IPS;
      process.env.SUPERADMIN_ALLOWED_IPS = "192.168.10.10,192.168.10.11";

      try {
        // 1. Try to access SuperAdmin endpoint with unauthorized IP
        const denyRes = await request(app)
          .patch("/api/v1/admin/settings")
          .set("Authorization", `Bearer ${superAdminToken}`)
          .set("X-Forwarded-For", "192.168.10.50")
          .send({ depositFeePercent: 0.9 });

        expect(denyRes.status).toBe(403);
        expect(denyRes.body.message).toContain("IP not authorized for SuperAdmin access");

        // 2. Access with authorized IP
        const allowRes = await request(app)
          .patch("/api/v1/admin/settings")
          .set("Authorization", `Bearer ${superAdminToken}`)
          .set("X-Forwarded-For", "192.168.10.11")
          .send({ depositFeePercent: 0.9 });

        expect(allowRes.status).toBe(200);
        expect(allowRes.body.success).toBe(true);
      } finally {
        if (originalAllowedIps) {
          process.env.SUPERADMIN_ALLOWED_IPS = originalAllowedIps;
        } else {
          delete process.env.SUPERADMIN_ALLOWED_IPS;
        }
      }
    });
  });
});
