import assert from "node:assert/strict";
import test from "node:test";

import { publicErrorResponse } from "./http.js";
import { createRtdnMessageStore, createRtdnRoute } from "./rtdn.js";

const PACKAGE_NAME = "com.makecdev.coderemotemanager";
const PRODUCT_ID = "pro_multi_account";
const TOKEN = "purchase-token-1234567890";
const NOW = Date.UTC(2026, 7, 2, 12, 0, 0);
const CONFIG = {
  packageName: PACKAGE_NAME,
  productId: PRODUCT_ID,
  pubsubOidc: {
    audience: "https://verifier.example.com/internal/google-play/rtdn",
    serviceAccounts: ["pubsub@example-project.iam.gserviceaccount.com"],
  },
};

test("RTDN authenticates before reading the body and invalidates a canceled one-time purchase", async () => {
  const calls = [];
  const route = createRtdnRoute({
    oidc: { async verifyRequest(_req, config) { calls.push("auth"); assert.equal(config, CONFIG.pubsubOidc); } },
    purchaseStore: { async invalidate(input) { calls.push("invalidate"); assert.deepEqual(input, identity("canceled")); } },
    messageStore: { async record(messageId) { calls.push("record"); assert.equal(messageId, "message-1"); return { duplicate: false }; } },
    config: CONFIG,
  });
  const response = await routeInput(route, envelope(oneTime(2)), () => calls.push("read"));

  assert.deepEqual(response, { status: 204, body: null });
  assert.deepEqual(calls, ["auth", "read", "invalidate", "record"]);
});

test("failed RTDN authentication never reads or parses attacker-controlled input", async () => {
  let reads = 0;
  const route = createRtdnRoute({
    oidc: { async verifyRequest() { throw Object.assign(new Error(), { status: 401, code: "unauthorized" }); } },
    purchaseStore: { async invalidate() { throw new Error("must not run"); } },
    messageStore: { async record() { throw new Error("must not run"); } },
    config: CONFIG,
  });
  await assert.rejects(routeInput(route, {}, () => { reads += 1; }), (error) => error.code === "unauthorized");
  assert.equal(reads, 0);
});

test("RTDN handles purchases, voided purchases, and duplicate delivery idempotently", async () => {
  const invalidations = [];
  const seen = new Set();
  const route = createRtdnRoute({
    oidc: { async verifyRequest() {} },
    purchaseStore: { async invalidate(input) { invalidations.push(input); } },
    messageStore: { async record(id) { const duplicate = seen.has(id); seen.add(id); return { duplicate }; } },
    config: CONFIG,
  });

  assert.equal((await routeInput(route, envelope(oneTime(1), "purchase"))).status, 204);
  assert.equal((await routeInput(route, envelope(voided(), "voided"))).status, 204);
  assert.equal((await routeInput(route, envelope(voided(), "voided"))).status, 204);
  assert.deepEqual(invalidations, [identity("refunded"), identity("refunded")]);
});

test("RTDN rejects wrong packages, products, notification kinds, and oversized data safely", async (t) => {
  const route = createRtdnRoute({
    oidc: { async verifyRequest() {} },
    purchaseStore: { async invalidate() { throw new Error("must not run"); } },
    messageStore: { async record() { throw new Error("must not run"); } },
    config: CONFIG,
  });
  const cases = [
    ["wrong package", envelope({ ...oneTime(2), packageName: "other.package" })],
    ["wrong product", envelope({ ...oneTime(2), oneTimeProductNotification: { ...oneTime(2).oneTimeProductNotification, sku: "other" } })],
    ["subscription", envelope({ version: "1.0", packageName: PACKAGE_NAME, eventTimeMillis: String(NOW), subscriptionNotification: {} })],
    ["invalid base64", { message: { messageId: "m", data: "%%%" }, subscription: "projects/p/subscriptions/s" }],
    ["oversized data", { message: { messageId: "m", data: "A".repeat(45_000) }, subscription: "projects/p/subscriptions/s" }],
  ];
  for (const [name, body] of cases) {
    await t.test(name, async () => {
      await assert.rejects(routeInput(route, body), (error) => {
        assert.deepEqual(publicErrorResponse(error), { status: 400, code: "invalid_rtdn_message" });
        assert.equal(JSON.stringify(error).includes(TOKEN), false);
        return true;
      });
    });
  }
});

test("processed message receipts use hashed IDs and expire after 30 days", async () => {
  const memory = fakeFirestore();
  const store = createRtdnMessageStore({ firestore: memory.firestore, now: () => NOW });
  assert.deepEqual(await store.record("private-message-id"), { duplicate: false });
  assert.deepEqual(await store.record("private-message-id"), { duplicate: true });
  assert.equal(memory.docs.size, 1);
  const [[path, value]] = memory.docs;
  assert.match(path, /^rtdnMessages\/[a-f0-9]{64}$/);
  assert.equal(path.includes("private-message-id"), false);
  assert.deepEqual(value, {
    schemaVersion: 1,
    processedAt: new Date(NOW),
    deleteAfter: new Date(NOW + (30 * 24 * 60 * 60 * 1000)),
  });
});

async function routeInput(route, body, onRead = () => {}) {
  return route({
    req: { method: "POST", headers: { authorization: "Bearer token" } },
    url: new URL("https://verifier.example.com/internal/google-play/rtdn"),
    readJson: async (_req, maxBytes) => { onRead(); assert.equal(maxBytes, 32 * 1024); return body; },
  });
}

function envelope(notification, messageId = "message-1") {
  return {
    message: { messageId, data: Buffer.from(JSON.stringify(notification)).toString("base64") },
    subscription: "projects/example-project/subscriptions/play-events",
  };
}

function oneTime(notificationType) {
  return {
    version: "1.0",
    packageName: PACKAGE_NAME,
    eventTimeMillis: String(NOW),
    oneTimeProductNotification: { version: "1.0", notificationType, purchaseToken: TOKEN, sku: PRODUCT_ID },
  };
}

function voided() {
  return {
    version: "1.0",
    packageName: PACKAGE_NAME,
    eventTimeMillis: String(NOW),
    voidedPurchaseNotification: { purchaseToken: TOKEN, orderId: "GPA.1234", productType: 2, refundType: 1 },
  };
}

function identity(reason) {
  return { packageName: PACKAGE_NAME, productId: PRODUCT_ID, purchaseToken: TOKEN, reason };
}

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