import assert from "node:assert/strict";
import { execSync } from "node:child_process";
import fs from "node:fs";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { after, before, describe, test } from "node:test";
import type { AddressInfo } from "node:net";
import type { Express } from "express";
import { callbackChecksum, webhookSignature, type CallbackPayload } from "./ipay";

const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "payhub-"));
const dbFile = path.join(tmp, "payments.db");
const productsFile = path.join(tmp, "products.json");

process.env.DATABASE_URL = `file:${dbFile}`;
process.env.PRODUCTS_FILE = productsFile;
process.env.IPAY_MERCHANT_WEB_TOKEN = "tok_test";
process.env.IPAY_SECRET = "sec_test";
process.env.IPAY_CHECKOUT_URL = "https://sandbox.ipay.lk/ipg/checkout";
process.env.APP_URL = "http://localhost:4100";
process.env.ADMIN_KEY = "admin-test-key";
process.env.NODE_ENV = "test";
process.env.PORT = "4100";

type Hit = { raw: string; signature: string | undefined; status: number };

let app: Express;
let server: http.Server;
let base = "";
let hook: http.Server;
let hookPort = 0;
const hits: Hit[] = [];
let failNextWebhook = false;

function writeProducts(): void {
  fs.writeFileSync(
    productsFile,
    JSON.stringify([
      {
        id: "loanbook",
        name: "LoanBook",
        orderPrefix: "OSLB",
        apiKey: "test-loanbook-key",
        webhookUrl: `http://127.0.0.1:${hookPort}/hook`,
        allowedReturnHosts: [`127.0.0.1:${hookPort}`, "localhost:5173"],
      },
      {
        id: "example-shop",
        name: "Example Shop",
        orderPrefix: "OSDM",
        apiKey: "test-shop-key",
        webhookUrl: `http://127.0.0.1:${hookPort}/shop`,
        allowedReturnHosts: [`127.0.0.1:${hookPort}`],
      },
    ])
  );
}

function signedCallback(input: {
  orderId: string;
  amount: string;
  status: string;
  reference?: string;
}): CallbackPayload {
  const payload: CallbackPayload = {
    transactionReference: input.reference ?? "IPG123456789",
    orderId: input.orderId,
    transactionTimeInMillis: "1710000000000",
    transactionAmount: input.amount,
    transactionStatus: input.status,
    transactionMessage: "Approved",
    merchantParam1: "loanbook",
    merchantParam2: "internal",
    checksum: "",
    creditedAmount: input.amount,
  };
  payload.checksum = callbackChecksum("sec_test", payload);
  return payload;
}

async function checkout(body: Record<string, unknown>, apiKey = "test-loanbook-key") {
  const response = await fetch(`${base}/api/v1/checkout`, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-API-Key": apiKey },
    body: JSON.stringify(body),
  });
  const json = (await response.json()) as Record<string, unknown>;
  return { response, json };
}

describe("payment hub http", { concurrency: 1 }, () => {
  before(async () => {
    hook = http.createServer((req, res) => {
      const chunks: Buffer[] = [];
      req.on("data", (chunk: Buffer) => chunks.push(chunk));
      req.on("end", () => {
        const fail = failNextWebhook;
        if (failNextWebhook) failNextWebhook = false;
        const status = fail ? 500 : 200;
        hits.push({
          raw: Buffer.concat(chunks).toString("utf8"),
          signature: typeof req.headers["x-payment-signature"] === "string" ? req.headers["x-payment-signature"] : undefined,
          status,
        });
        res.writeHead(status);
        res.end(fail ? "no" : "ok");
      });
    });
    await new Promise<void>((resolve) => hook.listen(0, "127.0.0.1", () => resolve()));
    hookPort = (hook.address() as AddressInfo).port;
    writeProducts();

    execSync("npx prisma migrate deploy", {
      cwd: path.resolve(__dirname, ".."),
      env: process.env,
      stdio: "pipe",
    });

    const loaded = await import("./app");
    app = loaded.createApp();
    server = http.createServer(app);
    await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()));
    base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
  });

  after(async () => {
    if (server) await new Promise<void>((resolve) => server.close(() => resolve()));
    if (hook) await new Promise<void>((resolve) => hook.close(() => resolve()));
    const { prisma } = await import("./db");
    await prisma.$disconnect();
  });

  test("health and landing page", async () => {
    const health = await fetch(`${base}/health`);
    assert.deepEqual(await health.json(), { status: "ok" });
    const home = await fetch(`${base}/`);
    const html = await home.text();
    assert.equal(home.status, 200);
    assert.match(html, /Optimize Solutions Payment Gateway/);
  });

  test("rejects checkout until iPay is configured", async () => {
    const token = process.env.IPAY_MERCHANT_WEB_TOKEN;
    process.env.IPAY_MERCHANT_WEB_TOKEN = "";
    try {
      const { response, json } = await checkout({
        productId: "loanbook",
        amount: 10,
        returnUrl: "http://localhost:5173/billing",
        cancelUrl: "http://localhost:5173/billing",
      });
      assert.equal(response.status, 503);
      assert.match(String(json.error), /not configured/i);
    } finally {
      process.env.IPAY_MERCHANT_WEB_TOKEN = token;
    }
  });

  test("rejects an open redirect and a foreign API key", async () => {
    const blocked = await checkout({
      productId: "loanbook",
      amount: 10,
      returnUrl: "https://evil.example/phish",
      cancelUrl: "http://localhost:5173/billing",
    });
    assert.equal(blocked.response.status, 400);

    const missing = await fetch(`${base}/api/v1/checkout`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: "{}",
    });
    assert.equal(missing.status, 401);

    const mismatched = await checkout(
      {
        productId: "loanbook",
        amount: 10,
        returnUrl: `http://127.0.0.1:${hookPort}/ok`,
        cancelUrl: `http://127.0.0.1:${hookPort}/ok`,
      },
      "test-shop-key"
    );
    assert.equal(mismatched.response.status, 403);
  });

  test("completes a checkout, verifies the callback once, and redirects", async () => {
    hits.length = 0;
    failNextWebhook = true;
    const started = await checkout({
      productId: "loanbook",
      amount: 4999,
      description: "LoanBook subscription <script>",
      customerName: "Jane",
      customerEmail: "jane@example.com",
      customerPhone: "0770000000",
      returnUrl: "http://localhost:5173/billing",
      cancelUrl: "http://localhost:5173/billing",
      metadata: { companyId: "co_1" },
    });
    assert.equal(started.response.status, 200);
    const orderId = String(started.json.orderId);
    assert.match(orderId, /^OSLB/);
    assert.equal(started.json.amount, "4999.00");
    assert.equal(started.json.productId, "loanbook");
    assert.equal(started.json.checkoutUrl, "https://sandbox.ipay.lk/ipg/checkout");
    assert.equal(started.json.payUrl, `http://localhost:4100/pay/${orderId}`);

    const fields = started.json.fields as Record<string, string>;
    assert.equal(fields.merchantWebToken, "tok_test");
    assert.equal(fields.totalAmount, "4999.00");
    assert.equal(fields.returnUrl, `http://localhost:4100/return?orderId=${orderId}`);
    assert.equal(fields.cancelUrl, `http://localhost:4100/cancel?orderId=${orderId}`);
    assert.equal(fields.merchantParam1, "loanbook");
    assert.equal(fields.merchantParam2.length > 0, true);
    assert.equal(fields.checksum.includes("sec_test"), false);

    const pay = await fetch(`${base}/pay/${orderId}`);
    const payHtml = await pay.text();
    assert.equal(pay.status, 200);
    assert.match(payHtml, /action="https:\/\/sandbox\.ipay\.lk\/ipg\/checkout"/);
    assert.match(payHtml, /name="checksum"/);
    assert.equal(payHtml.includes("sec_test"), false);
    assert.match(payHtml, /LoanBook subscription &lt;script&gt;/);

    const bad = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ ...signedCallback({ orderId, amount: "4999.00", status: "A" }), checksum: "nope" }),
    });
    assert.equal(bad.status, 400);

    const mismatch = signedCallback({ orderId, amount: "1.00", status: "A" });
    const mismatchRes = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(mismatch),
    });
    assert.equal(mismatchRes.status, 400);

    const callback = signedCallback({ orderId, amount: "4999.00", status: "A", reference: "IPG999" });
    const paid = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(callback),
    });
    assert.equal(paid.status, 200);
    assert.deepEqual(await paid.json(), { ok: true });
    assert.equal(hits.length, 2);
    assert.equal(hits[0]?.status, 500);
    assert.equal(hits[1]?.status, 200);
    assert.equal(hits[1]?.signature, webhookSignature("test-loanbook-key", hits[1]?.raw || ""));
    const delivered = JSON.parse(hits[1]?.raw || "{}") as Record<string, unknown>;
    assert.equal(delivered.status, "PAID");
    assert.equal(delivered.ipayStatus, "A");
    assert.equal(delivered.amount, "4999.00");
    assert.equal(delivered.ipayReference, "IPG999");
    assert.deepEqual(delivered.metadata, { companyId: "co_1" });

    const again = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(callback),
    });
    assert.equal(again.status, 200);
    assert.equal(hits.length, 2);

    const lookup = await fetch(`${base}/api/v1/payments/${orderId}`, {
      headers: { "X-API-Key": "test-loanbook-key" },
    });
    const payment = (await lookup.json()) as { status: string; webhookSentAt: string | null };
    assert.equal(payment.status, "PAID");
    assert.ok(payment.webhookSentAt);

    const hidden = await fetch(`${base}/api/v1/payments/${orderId}`, {
      headers: { "X-API-Key": "test-shop-key" },
    });
    assert.equal(hidden.status, 404);

    const back = await fetch(`${base}/return?orderId=${orderId}`, { redirect: "manual" });
    assert.equal(back.status, 302);
    const location = back.headers.get("location") || "";
    assert.match(location, /^http:\/\/localhost:5173\/billing\?/);
    assert.match(location, new RegExp(`orderId=${orderId}`));
    assert.match(location, /status=PAID/);
    assert.match(location, /ipayReference=IPG999/);

    const done = await fetch(`${base}/pay/${orderId}`);
    assert.match(await done.text(), /already paid/i);
  });

  test("marks pending settlement as paid and a decline as failed", async () => {
    const pending = await checkout({
      productId: "loanbook",
      amount: "15.50",
      returnUrl: "http://localhost:5173/billing",
      cancelUrl: "http://localhost:5173/billing",
    });
    const pendingId = String(pending.json.orderId);
    const pendingCallback = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams(signedCallback({ orderId: pendingId, amount: "15.50", status: "P", reference: "IPGP" })),
    });
    assert.equal(pendingCallback.status, 200);
    const pendingPayment = (await (
      await fetch(`${base}/api/v1/payments/${pendingId}`, { headers: { "X-API-Key": "test-loanbook-key" } })
    ).json()) as { status: string; ipayStatus: string };
    assert.equal(pendingPayment.status, "PAID");
    assert.equal(pendingPayment.ipayStatus, "P");

    const declined = await checkout({
      productId: "loanbook",
      amount: 20,
      returnUrl: "http://localhost:5173/billing",
      cancelUrl: "http://localhost:5173/billing",
    });
    const declinedId = String(declined.json.orderId);
    const declinedCallback = await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(signedCallback({ orderId: declinedId, amount: "20.00", status: "D", reference: "IPGD" })),
    });
    assert.equal(declinedCallback.status, 200);
    const declinedPayment = (await (
      await fetch(`${base}/api/v1/payments/${declinedId}`, { headers: { "X-API-Key": "test-loanbook-key" } })
    ).json()) as { status: string; ipayStatus: string };
    assert.equal(declinedPayment.status, "FAILED");
    assert.equal(declinedPayment.ipayStatus, "D");
  });

  test("cancel marks a pending payment and does not overwrite a paid one", async () => {
    const created = await checkout({
      productId: "loanbook",
      amount: 8,
      returnUrl: "http://localhost:5173/billing?from=hub",
      cancelUrl: "http://localhost:5173/billing?from=hub",
    });
    const orderId = String(created.json.orderId);
    const cancelled = await fetch(`${base}/cancel?orderId=${orderId}`, { redirect: "manual" });
    assert.equal(cancelled.status, 302);
    const location = cancelled.headers.get("location") || "";
    assert.match(location, /status=CANCELLED/);
    assert.match(location, /from=hub/);

    const paid = await checkout({
      productId: "loanbook",
      amount: 9,
      returnUrl: "http://localhost:5173/billing",
      cancelUrl: "http://localhost:5173/billing",
    });
    const paidId = String(paid.json.orderId);
    await fetch(`${base}/api/v1/ipay/callback`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(signedCallback({ orderId: paidId, amount: "9.00", status: "A" })),
    });
    const lateCancel = await fetch(`${base}/cancel?orderId=${paidId}`, { redirect: "manual" });
    assert.match(lateCancel.headers.get("location") || "", /status=PAID/);
  });

  test("admin key gates the payment list", async () => {
    const locked = await fetch(`${base}/admin/payments`);
    assert.match(await locked.text(), /Admin key/);

    const previous = process.env.ADMIN_KEY;
    process.env.ADMIN_KEY = "";
    try {
      const hidden = await fetch(`${base}/admin/payments`);
      assert.equal(hidden.status, 404);
    } finally {
      process.env.ADMIN_KEY = previous;
    }

    const posted = await fetch(`${base}/admin/payments`, {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({ key: "admin-test-key" }),
      redirect: "manual",
    });
    assert.equal(posted.status, 303);
    const cookie = posted.headers.get("set-cookie") || "";
    assert.match(cookie, /payment_admin=/);
    const token = /payment_admin=([^;]+)/.exec(cookie)?.[1] || "";
    const page = await fetch(`${base}/admin/payments`, { headers: { Cookie: `payment_admin=${token}` } });
    const html = await page.text();
    assert.match(html, /OSLB/);
    assert.match(html, /loanbook/);
  });
});
