import assert from "node:assert/strict";
import { generateKeyPairSync } from "node:crypto";
import test from "node:test";

import { createEntitlementSigner } from "./entitlement-signer.js";
import { publicErrorResponse } from "./http.js";
import { createPurchaseStateStore } from "./purchase-state.js";
import { createVerificationRateLimiter, createVerifyPurchaseRoute } from "./verify-purchase.js";

const PACKAGE_NAME = "com.makecdev.coderemotemanager";
const PRODUCT_ID = "pro_multi_account";
const PURCHASE_TOKEN = "purchase-token-1234567890";
const INPUT = {
  purchaseToken: PURCHASE_TOKEN,
  backendKeyThumbprint: "a".repeat(43),
  challengeHash: "b".repeat(43),
};
const NOW = Date.UTC(2026, 7, 2, 12, 0, 0);

test("verification route acknowledges, persists only hashed state, and returns only certificate fields", async () => {
  const memory = fakeFirestore();
  const purchaseStore = createPurchaseStateStore({ firestore: memory.firestore, now: () => NOW });
  const privateKey = generateKeyPairSync("ed25519").privateKey.export({ format: "pem", type: "pkcs8" });
  const signer = createEntitlementSigner({
    privateKey,
    keyId: "release-2026-01",
    issuer: "https://entitlements.example.com",
    now: () => NOW,
    randomUUID: () => "11111111-2222-4333-8444-555555555555",
  });
  let googleCalls = 0;
  const route = createVerifyPurchaseRoute({
    googlePlay: {
      async verifyPurchase(token) {
        googleCalls += 1;
        assert.equal(token, PURCHASE_TOKEN);
        return { state: "purchased", acknowledged: true, purchaseCompletionTime: "2026-08-02T11:55:00Z" };
      },
    },
    purchaseStore,
    signer,
    rateLimiter: createVerificationRateLimiter({ now: () => NOW }),
    packageName: PACKAGE_NAME,
    productId: PRODUCT_ID,
  });

  const response = await invoke(route, INPUT);
  assert.equal(response.status, 200);
  assert.deepEqual(Object.keys(response.body).sort(), ["certificate", "expiresAt"]);
  assert.match(response.body.certificate, /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/);
  assert.equal(response.body.expiresAt, "2026-09-01T12:00:00.000Z");
  assert.equal(googleCalls, 1);
  assert.equal(JSON.stringify([...memory.docs]).includes(PURCHASE_TOKEN), false);
  assert.equal(JSON.stringify(response).includes(PURCHASE_TOKEN), false);
});

test("verification input is exact, bounded, and never accepts request-controlled package or product", async (t) => {
  let googleCalls = 0;
  const route = basicRoute({
    googlePlay: { async verifyPurchase() { googleCalls += 1; return purchased(); } },
  });
  const cases = [
    ["additional field", { ...INPUT, packageName: PACKAGE_NAME }],
    ["wrong product attempt", { ...INPUT, productId: "another_product" }],
    ["short token", { ...INPUT, purchaseToken: "short" }],
    ["control character", { ...INPUT, purchaseToken: `purchase-token-${String.fromCharCode(1)}-value` }],
    ["non-ASCII token", { ...INPUT, purchaseToken: "purchase-token-결제-123456" }],
    ["oversized token", { ...INPUT, purchaseToken: "x".repeat(4097) }],
    ["bad thumbprint", { ...INPUT, backendKeyThumbprint: "a".repeat(42) }],
    ["bad challenge hash", { ...INPUT, challengeHash: "not/base64url" }],
  ];
  for (const [name, input] of cases) {
    await t.test(name, async () => {
      await assert.rejects(invoke(route, input), (error) => {
        assert.deepEqual(publicErrorResponse(error), { status: 400, code: "invalid_verification_request" });
        return true;
      });
    });
  }
  assert.equal(googleCalls, 0);
});

test("fallback limiter independently bounds client IP and purchase-token hash", () => {
  let currentTime = NOW;
  const limiter = createVerificationRateLimiter({
    now: () => currentTime,
    windowMs: 60_000,
    ipLimit: 2,
    tokenLimit: 1,
  });

  assert.equal(limiter.consume({ clientIp: "203.0.113.1", purchaseKey: "a".repeat(64) }).allowed, true);
  assert.equal(limiter.consume({ clientIp: "203.0.113.2", purchaseKey: "a".repeat(64) }).allowed, false);
  assert.equal(limiter.consume({ clientIp: "203.0.113.1", purchaseKey: "b".repeat(64) }).allowed, true);
  assert.equal(limiter.consume({ clientIp: "203.0.113.1", purchaseKey: "c".repeat(64) }).allowed, false);

  currentTime += 60_000;
  assert.equal(limiter.consume({ clientIp: "203.0.113.1", purchaseKey: "a".repeat(64) }).allowed, true);
  assert.equal(JSON.stringify(limiter).includes("203.0.113.1"), false);
});

test("fallback limiter releases expired capacity for new clients and tokens", () => {
  let currentTime = NOW;
  const limiter = createVerificationRateLimiter({
    now: () => currentTime,
    windowMs: 60_000,
    ipLimit: 1,
    tokenLimit: 1,
    maxEntries: 1,
  });
  assert.equal(limiter.consume({ clientIp: "203.0.113.1", purchaseKey: "a".repeat(64) }).allowed, true);
  currentTime += 60_000;
  assert.equal(limiter.consume({ clientIp: "203.0.113.2", purchaseKey: "b".repeat(64) }).allowed, true);
});

test("a previously invalidated purchase cannot issue a certificate on replay", async () => {
  const memory = fakeFirestore();
  const purchaseStore = createPurchaseStateStore({ firestore: memory.firestore, now: () => NOW });
  await purchaseStore.invalidate({
    packageName: PACKAGE_NAME,
    productId: PRODUCT_ID,
    purchaseToken: PURCHASE_TOKEN,
    reason: "refunded",
  });
  let signCalls = 0;
  const route = basicRoute({
    purchaseStore,
    signer: { sign() { signCalls += 1; return { certificate: "never", expiresAt: "never" }; } },
  });

  await assert.rejects(invoke(route, INPUT), (error) => {
    assert.deepEqual(publicErrorResponse(error), { status: 402, code: "purchase_invalidated" });
    return true;
  });
  assert.equal(signCalls, 0);
  assert.equal(JSON.stringify([...memory.docs]).includes(PURCHASE_TOKEN), false);
});

test("upstream and internal failures become fixed public codes without the purchase token", async (t) => {
  const cases = [
    ["pending", Object.assign(new Error(`pending ${PURCHASE_TOKEN}`), { code: "purchase_pending", status: 409 }), 409, "purchase_pending"],
    ["Google outage", Object.assign(new Error(`Google raw ${PURCHASE_TOKEN}`), { code: "google_play_unavailable", status: 503 }), 503, "verification_unavailable"],
    ["unknown", new Error(`private failure ${PURCHASE_TOKEN}`), 503, "verification_unavailable"],
  ];
  for (const [name, failure, status, code] of cases) {
    await t.test(name, async () => {
      const route = basicRoute({ googlePlay: { async verifyPurchase() { throw failure; } } });
      await assert.rejects(invoke(route, INPUT), (error) => {
        assert.deepEqual(publicErrorResponse(error), { status, code });
        assert.equal(JSON.stringify(error).includes(PURCHASE_TOKEN), false);
        return true;
      });
    });
  }
});

function basicRoute(overrides = {}) {
  return createVerifyPurchaseRoute({
    googlePlay: { async verifyPurchase() { return purchased(); } },
    purchaseStore: {
      async recordActive() { return { purchaseKey: "f".repeat(64), state: "active" }; },
    },
    signer: {
      sign() { return { certificate: "header.claims.signature", expiresAt: "2026-09-01T12:00:00.000Z" }; },
    },
    rateLimiter: { consume() { return { allowed: true }; } },
    packageName: PACKAGE_NAME,
    productId: PRODUCT_ID,
    ...overrides,
  });
}

function purchased() {
  return { state: "purchased", acknowledged: true, purchaseCompletionTime: "2026-08-02T11:55:00Z" };
}

function invoke(route, input, clientIp = "203.0.113.10") {
  return route({
    req: { method: "POST" },
    url: new URL("https://entitlements.example.com/v1/entitlements/google-play/verify"),
    readJson: async () => input,
    clientIp,
  });
}

function fakeFirestore() {
  const docs = new Map();
  const firestore = {
    collection(name) {
      return { doc(id) { return { path: `${name}/${id}` }; } };
    },
    async runTransaction(operation) {
      const pending = [];
      const transaction = {
        async get(reference) {
          const value = docs.get(reference.path);
          return { exists: value !== undefined, data: () => structuredClone(value) };
        },
        set(reference, value) { pending.push([reference.path, structuredClone(value)]); },
      };
      const result = await operation(transaction);
      for (const [path, value] of pending) docs.set(path, value);
      return result;
    },
  };
  return { docs, firestore };
}
