import { test } from "node:test";
import assert from "node:assert/strict";
import { INSTANCE_TTL, mintClaims, signToken, verifyToken } from "../src/runnerToken.ts";

const SECRET = "test-secret";

test("sign/verify round-trip", async () => {
  const claims = mintClaims({ poolId: "p1", instanceId: "runner-1", ttlSeconds: 60 });
  const token = await signToken(SECRET, claims);
  const verdict = await verifyToken(SECRET, token);
  assert.equal(verdict.ok, true);
  if (verdict.ok) {
    assert.equal(verdict.claims.poolId, "p1");
    assert.equal(verdict.claims.instanceId, "runner-1");
    assert.equal(verdict.claims.typ, "i");
  }
});

test("tampered payload is rejected", async () => {
  const token = await signToken(SECRET, mintClaims({ poolId: "p1", instanceId: "runner-2", ttlSeconds: 60 }));
  const [payload, sig] = token.split(".");
  const forged = JSON.parse(Buffer.from(payload, "base64url").toString());
  forged.instanceId = "runner-1"; // privilege swap
  const forgedToken = `${Buffer.from(JSON.stringify(forged)).toString("base64url")}.${sig}`;
  const verdict = await verifyToken(SECRET, forgedToken);
  assert.equal(verdict.ok, false);
});

test("wrong secret is rejected", async () => {
  const token = await signToken(SECRET, mintClaims({ poolId: "p1", instanceId: "runner-1", ttlSeconds: 60 }));
  const verdict = await verifyToken("other-secret", token);
  assert.equal(verdict.ok, false);
});

test("expired token is rejected", async () => {
  const claims = mintClaims({ poolId: "p1", instanceId: "runner-1", ttlSeconds: 60, now: Date.now() - 120_000 });
  const token = await signToken(SECRET, claims);
  const verdict = await verifyToken(SECRET, token);
  assert.equal(verdict.ok, false);
  if (!verdict.ok) assert.equal(verdict.error, "expired");
});

test("malformed tokens are rejected", async () => {
  for (const bad of ["", "nodot", ".leadingdot", "a.b"]) {
    const verdict = await verifyToken(SECRET, bad);
    assert.equal(verdict.ok, false, `expected reject: ${JSON.stringify(bad)}`);
  }
});

test("non-instance typ is rejected", async () => {
  // Hand-build claims with a foreign typ (e.g. a relay membership smuggled over).
  const claims = { poolId: "p1", instanceId: "runner-1", typ: "m", iat: 0, exp: Math.floor(Date.now() / 1000) + 60 };
  const payload = Buffer.from(JSON.stringify(claims)).toString("base64url");
  const key = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(SECRET),
    { name: "HMAC", hash: "SHA-256" },
    false,
    ["sign"],
  );
  const sig = Buffer.from(await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))).toString(
    "base64url",
  );
  const verdict = await verifyToken(SECRET, `${payload}.${sig}`);
  assert.equal(verdict.ok, false);
  if (!verdict.ok) assert.equal(verdict.error, "badtype");
});

test("instance TTL is ~30 days", () => {
  assert.equal(INSTANCE_TTL, 30 * 24 * 60 * 60);
});
