import test from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, statSync, writeFileSync, readFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { TokenStore } from "../auth/token-store.js";
function makeAccountsDir() {
    const dir = mkdtempSync(join(tmpdir(), "outlook-test-"));
    return {
        dir,
        cleanup: () => rmSync(dir, { recursive: true, force: true }),
    };
}
test("TokenStore round-trip preserves the blob", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-A", dir);
        store.store("access-1", "refresh-1", 3600, {
            graphUserId: "user-1",
            scopes: ["Mail.Read", "Calendars.Read"],
        });
        const fresh = new TokenStore("acct-A", dir);
        const blob = fresh.read();
        assert.ok(blob, "expected blob to exist after round-trip");
        assert.equal(blob.accessToken, "access-1");
        assert.equal(blob.refreshToken, "refresh-1");
        assert.equal(blob.graphUserId, "user-1");
        assert.deepEqual(blob.scopes, ["Mail.Read", "Calendars.Read"]);
    }
    finally {
        cleanup();
    }
});
test("TokenStore corrupt cipher fails loud", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-X", dir);
        store.store("access-X", "refresh-X", 3600);
        // Tamper with the ciphertext on disk.
        const tokensPath = join(dir, "acct-X", "secrets", "outlook", "tokens.enc");
        writeFileSync(tokensPath, "garbage-not-a-cipher");
        const fresh = new TokenStore("acct-X", dir);
        assert.throws(() => fresh.read(), /token-decrypt-failed|bad decrypt|wrong final block length|invalid|wrong tag/i);
    }
    finally {
        cleanup();
    }
});
test("TokenStore per-account isolation — two stores never decrypt each other", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const storeA = new TokenStore("acct-A", dir);
        const storeB = new TokenStore("acct-B", dir);
        storeA.store("access-A", "refresh-A", 3600, { graphUserId: "user-A", scopes: [] });
        storeB.store("access-B", "refresh-B", 3600, { graphUserId: "user-B", scopes: [] });
        // Read each independently.
        const blobA = new TokenStore("acct-A", dir).read();
        const blobB = new TokenStore("acct-B", dir).read();
        assert.equal(blobA?.accessToken, "access-A");
        assert.equal(blobB?.accessToken, "access-B");
        assert.notEqual(blobA?.accessToken, blobB?.accessToken);
        // Sentinel: swap the .key files between accounts. The store should now
        // fail to decrypt — proves keys are genuinely per-account, not shared.
        const keyA = readFileSync(join(dir, "acct-A", "secrets", "outlook", ".key"), "utf-8");
        const keyB = readFileSync(join(dir, "acct-B", "secrets", "outlook", ".key"), "utf-8");
        assert.notEqual(keyA, keyB, "keys must differ across accounts");
        writeFileSync(join(dir, "acct-A", "secrets", "outlook", ".key"), keyB);
        const tampered = new TokenStore("acct-A", dir);
        assert.throws(() => tampered.read(), /bad decrypt|wrong final|invalid|token-decrypt-failed/i);
    }
    finally {
        cleanup();
    }
});
test("TokenStore directory permissions are 0700, file permissions are 0600", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-mode", dir);
        store.store("a", "r", 3600);
        const secretsDir = join(dir, "acct-mode", "secrets", "outlook");
        const dirMode = statSync(secretsDir).mode & 0o777;
        assert.equal(dirMode, 0o700, "secrets dir must be 0700");
        const keyMode = statSync(join(secretsDir, ".key")).mode & 0o777;
        assert.equal(keyMode, 0o600, "key file must be 0600");
        const tokensMode = statSync(join(secretsDir, "tokens.enc")).mode & 0o777;
        assert.equal(tokensMode, 0o600, "tokens file must be 0600");
    }
    finally {
        cleanup();
    }
});
test("TokenStore.needsRefresh returns true within the threshold window", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-r", dir);
        store.store("a", "r", 60); // 60s expiry — well under the 300s threshold
        assert.equal(store.needsRefresh(), true);
    }
    finally {
        cleanup();
    }
});
test("TokenStore.needsRefresh returns false outside the threshold window", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-f", dir);
        store.store("a", "r", 3600); // 1h expiry
        assert.equal(store.needsRefresh(), false);
    }
    finally {
        cleanup();
    }
});
test("TokenStore.clear removes the token blob but leaves the key", () => {
    const { dir, cleanup } = makeAccountsDir();
    try {
        const store = new TokenStore("acct-c", dir);
        store.store("a", "r", 3600);
        store.clear();
        assert.equal(store.read(), null);
        // Key file still present — re-register reuses it.
        const fresh = new TokenStore("acct-c", dir);
        fresh.store("a2", "r2", 3600);
        assert.equal(fresh.read()?.accessToken, "a2");
    }
    finally {
        cleanup();
    }
});
//# sourceMappingURL=token-store.test.js.map