import assert from "node:assert/strict";
import { createHash, generateKeyPairSync, sign } from "node:crypto";
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";

import { createEntitlementStore } from "./entitlement-store.js";

const START = Date.UTC(2026, 7, 2, 12, 0, 0);

test("explicit developer Pro mode exposes a purchased account without persisting a certificate", async () => {
  const directory = await mkdtemp(join(tmpdir(), "crm-entitlement-store-developer-pro-"));
  const accounts = [{ id: "account-1" }, { id: "account-2" }];
  const store = createEntitlementStore({
    dataDir: directory,
    publicKeys: {},
    accountsProvider: () => accounts,
    now: () => START,
    developerPro: true,
  });

  try {
    await store.initialize();

    assert.deepEqual(await store.getStatus(), {
      tier: "pro",
      state: "active",
      maxAccounts: 5,
      configuredAccounts: 2,
      expiresAt: null,
      graceEndsAt: null,
    });
    assert.deepEqual(store.getAccountExecutionAccess("account-2"), {
      allowed: true,
      entitlementLocked: false,
    });

    const persisted = JSON.parse(await readFile(join(directory, "security", "entitlement.json"), "utf8"));
    assert.equal(persisted.certificate, null);
    assert.equal(persisted.claims, null);
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
});

test("initialization persists one private installation key and exposes only its stable thumbprint", async () => {
  await withStore(async ({ directory, publicKeys, store, now }) => {
    await store.initialize();
    const firstChallenge = await store.issueChallenge();
    const keyPath = join(directory, "security", "entitlement-key.json");
    const keyRecord = JSON.parse(await readFile(keyPath, "utf8"));
    const publicStatus = await store.getStatus();

    assert.match(keyRecord.privateKey, /^[A-Za-z0-9_-]+$/);
    assert.match(keyRecord.publicKey, /^[A-Za-z0-9_-]+$/);
    assert.equal(firstChallenge.backendKeyThumbprint, keyRecord.backendKeyThumbprint);
    assert.equal(JSON.stringify(publicStatus).includes(keyRecord.privateKey), false);
    assert.equal(JSON.stringify(publicStatus).includes(keyRecord.publicKey), false);

    const details = await stat(keyPath);
    if (process.platform !== "win32") assert.equal(details.mode & 0o077, 0);
    assert.deepEqual((await readdir(join(directory, "security"))).filter((name) => name.endsWith(".pending")), []);

    const restarted = createEntitlementStore({
      dataDir: directory,
      publicKeys,
      accountsProvider: () => [{ id: "account-1" }],
      now,
    });
    await restarted.initialize();
    const restartedChallenge = await restarted.issueChallenge();
    assert.equal(restartedChallenge.backendKeyThumbprint, firstChallenge.backendKeyThumbprint);
  });
});

test("challenges are opaque, hash-only, five-minute, and single-use for activation", async () => {
  await withStore(async ({ directory, signer, store, advance }) => {
    await store.initialize();
    const first = await store.issueChallenge();
    const second = await store.issueChallenge();
    assert.match(first.challenge, /^[A-Za-z0-9_-]{32,}$/);
    assert.notEqual(second.challenge, first.challenge);

    const certificate = signer.certificate({
      backendKeyThumbprint: first.backendKeyThumbprint,
      challenge: first.challenge,
    });
    const activated = await store.activate({ challenge: first.challenge, certificate });
    assert.equal(activated.tier, "pro");
    assert.equal(activated.state, "active");

    const persisted = await readFile(join(directory, "security", "entitlement.json"), "utf8");
    assert.equal(persisted.includes(first.challenge), false);
    assert.equal(persisted.includes(second.challenge), false);
    assert.equal(persisted.includes(hash(first.challenge)), true);
    assert.equal(persisted.includes(certificate), true);
    assert.equal(JSON.stringify(activated).includes(certificate), false);

    assert.deepEqual(
      await store.activate({ challenge: first.challenge, certificate }),
      activated
    );
    await assert.rejects(
      store.activate({
        challenge: first.challenge,
        certificate: signer.certificate({
          backendKeyThumbprint: first.backendKeyThumbprint,
          challenge: first.challenge,
          jti: "another-certificate",
        }),
      }),
      (error) => error?.code === "entitlement_challenge_used"
    );

    advance(5 * 60 * 1000);
    await assert.rejects(
      store.activate({
        challenge: second.challenge,
        certificate: signer.certificate({
          backendKeyThumbprint: second.backendKeyThumbprint,
          challenge: second.challenge,
        }),
      }),
      (error) => error?.code === "entitlement_challenge_expired"
    );
  });
});

test("activation requires both the issued challenge and this installation key", async () => {
  await withStore(async ({ signer, store }) => {
    await store.initialize();
    const issued = await store.issueChallenge();

    await assert.rejects(
      store.activate({
        challenge: issued.challenge,
        certificate: signer.certificate({
          backendKeyThumbprint: "another-installation",
          challenge: issued.challenge,
        }),
      }),
      (error) => error?.code === "invalid_entitlement_certificate"
    );
    await assert.rejects(
      store.activate({
        challenge: issued.challenge,
        certificate: signer.certificate({
          backendKeyThumbprint: issued.backendKeyThumbprint,
          challenge: "another-challenge",
        }),
      }),
      (error) => error?.code === "invalid_entitlement_certificate"
    );
  });
});

test("expired challenge history is capped", async () => {
  await withStore(async ({ directory, store, advance }) => {
    await store.initialize();
    for (let index = 0; index < 40; index += 1) {
      await store.issueChallenge();
      advance(5 * 60 * 1000);
    }

    const persisted = JSON.parse(await readFile(join(directory, "security", "entitlement.json"), "utf8"));
    assert.ok(persisted.challenges.length <= 32);
  });
});

test("store policy methods use the persisted entitlement", async () => {
  await withStore(async ({ signer, store }) => {
    await store.initialize();
    assert.throws(() => store.assertCanAddExecutionAccount(), (error) => error?.code === "pro_required");
    const issued = await store.issueChallenge();
    await store.activate({
      challenge: issued.challenge,
      certificate: signer.certificate({
        backendKeyThumbprint: issued.backendKeyThumbprint,
        challenge: issued.challenge,
      }),
    });

    assert.doesNotThrow(() => store.assertCanAddExecutionAccount());
    assert.equal(store.getAccountExecutionAccess("account-2").allowed, true);
  }, { accounts: [{ id: "account-1" }, { id: "account-2" }] });
});

test("persisted Pro access survives restart and refund expiry downgrades without deleting accounts", async () => {
  const accounts = [
    ...Array.from({ length: 6 }, (_, index) => ({ id: `account-${index + 1}` })),
    { id: "history", historyOnly: true },
  ];
  await withStore(async ({ directory, publicKeys, signer, store, now, advance }) => {
    await store.initialize();
    const issued = await store.issueChallenge();
    await store.activate({
      challenge: issued.challenge,
      certificate: signer.certificate({
        backendKeyThumbprint: issued.backendKeyThumbprint,
        challenge: issued.challenge,
      }),
    });

    advance(30 * 24 * 60 * 60 * 1000);
    const restarted = createEntitlementStore({
      dataDir: directory,
      publicKeys,
      accountsProvider: () => accounts,
      now,
    });
    await restarted.initialize();

    assert.equal((await restarted.getStatus()).configuredAccounts, 6);
    assert.equal((await restarted.getStatus()).state, "grace");
    for (const account of accounts.slice(0, 5)) {
      assert.equal(restarted.getAccountExecutionAccess(account.id).allowed, true);
    }
    assert.equal(restarted.getAccountExecutionAccess("account-6").allowed, false);

    advance(7 * 24 * 60 * 60 * 1000);
    const expiredRestart = createEntitlementStore({
      dataDir: directory,
      publicKeys,
      accountsProvider: () => accounts,
      now,
    });
    await expiredRestart.initialize();
    const expired = await expiredRestart.getStatus();
    assert.equal(expired.tier, "free");
    assert.equal(expired.state, "expired");
    assert.equal(expiredRestart.getAccountExecutionAccess("account-1").allowed, true);
    assert.equal(expiredRestart.getAccountExecutionAccess("account-2").allowed, false);
    assert.deepEqual(accounts, [
      ...Array.from({ length: 6 }, (_, index) => ({ id: `account-${index + 1}` })),
      { id: "history", historyOnly: true },
    ]);
  }, { accounts });
});

test("restart fails closed when persisted certificate claims are tampered", async () => {
  await withStore(async ({ directory, publicKeys, signer, store, now }) => {
    await store.initialize();
    const issued = await store.issueChallenge();
    await store.activate({
      challenge: issued.challenge,
      certificate: signer.certificate({
        backendKeyThumbprint: issued.backendKeyThumbprint,
        challenge: issued.challenge,
      }),
    });

    const statePath = join(directory, "security", "entitlement.json");
    const state = JSON.parse(await readFile(statePath, "utf8"));
    state.claims.maxAccounts = 1;
    await writeFile(statePath, JSON.stringify(state));

    const restarted = createEntitlementStore({
      dataDir: directory,
      publicKeys,
      accountsProvider: () => [{ id: "account-1" }, { id: "account-2" }],
      now,
    });
    await assert.rejects(
      restarted.initialize(),
      (error) => error?.code === "entitlement_state_invalid"
    );
  });
});

async function withStore(run, { accounts = [{ id: "account-1" }] } = {}) {
  const directory = await mkdtemp(join(tmpdir(), "crm-entitlement-store-"));
  const signingKeys = generateKeyPairSync("ed25519");
  const publicKeys = { "release-2026-01": signingKeys.publicKey };
  let currentTime = START;
  const now = () => currentTime;
  const signer = {
    certificate({ backendKeyThumbprint, challenge, jti = "certificate-id" }) {
      return signCertificate(signingKeys.privateKey, {
        backendKeyThumbprint,
        challengeHash: hash(challenge),
        now: currentTime,
        jti,
      });
    },
  };
  const store = createEntitlementStore({
    dataDir: directory,
    publicKeys,
    accountsProvider: () => accounts,
    now,
  });
  try {
    await run({
      directory,
      publicKeys,
      signer,
      store,
      now,
      advance(milliseconds) { currentTime += milliseconds; },
    });
  } finally {
    await rm(directory, { recursive: true, force: true });
  }
}

function signCertificate(privateKey, { backendKeyThumbprint, challengeHash, now, jti }) {
  const header = { alg: "EdDSA", kid: "release-2026-01" };
  const nowSeconds = Math.floor(now / 1000);
  const claims = {
    iss: "crm-entitlement-verifier",
    aud: "code-remote-manager-backend",
    productId: "pro_multi_account",
    maxAccounts: 5,
    backendKeyThumbprint,
    challengeHash,
    iat: nowSeconds,
    exp: nowSeconds + (30 * 24 * 60 * 60),
    jti,
  };
  const signingInput = `${encode(header)}.${encode(claims)}`;
  const signature = sign(null, Buffer.from(signingInput), privateKey).toString("base64url");
  return `${signingInput}.${signature}`;
}

function encode(value) {
  return Buffer.from(JSON.stringify(value)).toString("base64url");
}

function hash(value) {
  return createHash("sha256").update(value).digest("base64url");
}
