"use strict";
/**
 * test_distributed_lock — Story 6.4 regression suite.
 *
 * Four scenarios:
 *   1. Acquire + release happy path; peek round-trips the record
 *   2. Collision — second acquire on same key fails while first holds
 *   3. TTL expiry — stale lock can be taken over after ttlMs elapses
 *      (test uses a tiny ttlMs and a controlled busy-wait, no sleep)
 *   4. Renew + steal-detection — renewing a lock we no longer own
 *      (file overwritten by an attacker simulation) returns false
 *
 * Prints TEST_DISTRIBUTED_LOCK_OK on full pass.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
const child_process_1 = require("child_process");
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const DistributedLock_1 = require("./orchestrator/util/DistributedLock");
const AtomicWriter_1 = require("./orchestrator/util/AtomicWriter");
function assert(cond, msg) {
    if (!cond)
        throw new Error(`assertion failed: ${msg}`);
}
function mkTmp() {
    return fs.mkdtempSync(path.join(os.tmpdir(), 'lock-test-'));
}
function rmTmp(dir) {
    try {
        fs.rmSync(dir, { recursive: true, force: true });
    }
    catch { /* ignore */ }
}
function busyWait(ms) {
    const deadline = Date.now() + ms;
    while (Date.now() < deadline) { /* spin */ }
}
// ─────────────────────────────────────────────────────────────
// Scenario 1 — acquire + release happy path
// ─────────────────────────────────────────────────────────────
function scenarioHappyPath() {
    const tmp = mkTmp();
    try {
        const lock = new DistributedLock_1.DistributedLock({ baseDir: path.join(tmp, 'locks') });
        const r = lock.acquire('mission-001', 30_000, { startHeartbeat: false });
        assert(r.ok, `expected ok=true, got: ${JSON.stringify(r)}`);
        assert(typeof r.lockId === 'string' && r.lockId.length > 0, 'lockId must be present');
        assert(fs.existsSync(r.filePath), 'lock file must exist on disk');
        const peeked = lock.peek('mission-001');
        assert(peeked !== null, 'peek must return record while held');
        assert(peeked.pid === process.pid, 'pid round-trip');
        assert(peeked.key === 'mission-001', 'key round-trip');
        assert(peeked.ttlMs === 30_000, 'ttlMs round-trip');
        lock.release(r.lockId);
        assert(lock.peek('mission-001') === null, 'peek must return null after release');
        console.log('✅ scenario 1: acquire + release + peek round-trip');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 2 — collision while held
// ─────────────────────────────────────────────────────────────
function scenarioCollision() {
    const tmp = mkTmp();
    try {
        const baseDir = path.join(tmp, 'locks');
        const lockA = new DistributedLock_1.DistributedLock({ baseDir });
        const lockB = new DistributedLock_1.DistributedLock({ baseDir });
        const a = lockA.acquire('shared', 30_000, { startHeartbeat: false });
        assert(a.ok, 'A must acquire');
        const b = lockB.acquire('shared', 30_000, { startHeartbeat: false });
        assert(!b.ok, `B must collide, got ok=true: ${JSON.stringify(b)}`);
        assert(b.ok === false && b.error === 'collision', `expected error=collision, got ${JSON.stringify(b)}`);
        assert(b.ok === false && b.heldBy?.pid === process.pid, 'heldBy must report current pid');
        lockA.release(a.lockId);
        const bAgain = lockB.acquire('shared', 30_000, { startHeartbeat: false });
        assert(bAgain.ok, 'B must acquire after A releases');
        lockB.release(bAgain.lockId);
        console.log('✅ scenario 2: collision while held');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 3 — TTL expiry takeover
// ─────────────────────────────────────────────────────────────
function scenarioTtlExpiry() {
    const tmp = mkTmp();
    try {
        const baseDir = path.join(tmp, 'locks');
        const lockA = new DistributedLock_1.DistributedLock({ baseDir });
        const lockB = new DistributedLock_1.DistributedLock({ baseDir });
        // A acquires with a tiny TTL, no heartbeat so it goes stale
        const a = lockA.acquire('expiring', 50, { startHeartbeat: false });
        assert(a.ok, 'A must acquire');
        // Immediate B attempt collides
        const bNow = lockB.acquire('expiring', 1000, { startHeartbeat: false });
        assert(!bNow.ok, 'B must collide before TTL expires');
        // Wait past TTL
        busyWait(70);
        const bLater = lockB.acquire('expiring', 1000, { startHeartbeat: false });
        assert(bLater.ok, `B must take over after TTL, got: ${JSON.stringify(bLater)}`);
        assert(bLater.ok && bLater.lockId !== a.lockId, 'B must have a new lockId');
        // After takeover, A's renew should fail (different lockId on disk now)
        const aRenew = lockA.renew(a.lockId);
        assert(aRenew === false, "A's renew must fail after B took over");
        lockB.release(bLater.lockId);
        console.log('✅ scenario 3: TTL expiry takeover');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 4 — renew detects external overwrite (steal-detection)
// ─────────────────────────────────────────────────────────────
function scenarioStealDetection() {
    const tmp = mkTmp();
    try {
        const baseDir = path.join(tmp, 'locks');
        const lock = new DistributedLock_1.DistributedLock({ baseDir });
        const a = lock.acquire('steal-test', 30_000, { startHeartbeat: false });
        assert(a.ok, 'must acquire');
        // Simulate an attacker / TTL takeover: overwrite the lock file with a
        // different lockId
        const stolen = {
            pid: 99999,
            host: 'attacker',
            lockId: 'stolen-id',
            key: 'steal-test',
            acquiredAt: Date.now(),
            ts: Date.now(),
            ttlMs: 30_000,
        };
        (0, AtomicWriter_1.writeJsonAtomic)(a.filePath, stolen);
        const renewed = lock.renew(a.lockId);
        assert(renewed === false, 'renew must return false when our lockId no longer matches on disk');
        // peek shows the stolen record (different pid)
        const after = lock.peek('steal-test');
        assert(after?.pid === 99999, 'peek reflects the new holder');
        console.log('✅ scenario 4: renew detects external steal');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Scenario 5 — liveness reclaim of a dead-pid lock (same host)
// ─────────────────────────────────────────────────────────────
/** A pid that is guaranteed dead: spawn a trivial child, let spawnSync
 *  block until it exits AND reaps it, then return its (now-freed) pid. */
function deadPid() {
    const r = (0, child_process_1.spawnSync)(process.execPath, ['-e', '0']);
    if (typeof r.pid !== 'number')
        throw new Error('could not obtain a dead pid');
    return r.pid;
}
function scenarioLivenessReclaim() {
    const tmp = mkTmp();
    try {
        const baseDir = path.join(tmp, 'locks');
        const lock = new DistributedLock_1.DistributedLock({ baseDir });
        // Plant a lock owned by a DEAD pid on THIS host, with a long TTL that
        // has NOT expired — the only reason it's reclaimable is liveness.
        const planted = {
            pid: deadPid(),
            host: os.hostname(),
            lockId: 'zombie-id',
            key: 'crashed-mission',
            acquiredAt: Date.now(),
            ts: Date.now(),
            ttlMs: 400_000,
        };
        const filePath = path.join(baseDir, 'crashed-mission.lock');
        fs.mkdirSync(baseDir, { recursive: true });
        (0, AtomicWriter_1.writeJsonAtomic)(filePath, planted);
        const r = lock.acquire('crashed-mission', 30_000, { startHeartbeat: false });
        assert(r.ok, `dead-pid lock on same host must be reclaimed, got: ${JSON.stringify(r)}`);
        assert(r.ok && r.lockId !== 'zombie-id', 'reclaim installs a fresh lockId');
        lock.release(r.lockId);
        // Guard A — a LIVE pid (our own) on the same host within TTL must NOT
        // be stolen; reclaim only triggers on provable death.
        const live = {
            ...planted, pid: process.pid, lockId: 'live-id', key: 'live-mission', acquiredAt: Date.now(), ts: Date.now(),
        };
        const livePath = path.join(baseDir, 'live-mission.lock');
        (0, AtomicWriter_1.writeJsonAtomic)(livePath, live);
        const rb = lock.acquire('live-mission', 30_000, { startHeartbeat: false });
        assert(!rb.ok && rb.error === 'collision', `live holder must NOT be reclaimed, got: ${JSON.stringify(rb)}`);
        // Guard B — a dead pid on a REMOTE host is unprobeable, so within TTL
        // it must fall back to collision (only TTL can reclaim cross-host).
        const remote = {
            ...planted, host: `${os.hostname()}-elsewhere`, lockId: 'remote-id', key: 'remote-mission', acquiredAt: Date.now(), ts: Date.now(),
        };
        const remotePath = path.join(baseDir, 'remote-mission.lock');
        (0, AtomicWriter_1.writeJsonAtomic)(remotePath, remote);
        const rc = lock.acquire('remote-mission', 30_000, { startHeartbeat: false });
        assert(!rc.ok && rc.error === 'collision', `cross-host lock must fall back to TTL, got: ${JSON.stringify(rc)}`);
        console.log('✅ scenario 5: liveness reclaim (dead pid same-host steals; live + cross-host do not)');
    }
    finally {
        rmTmp(tmp);
    }
}
// ─────────────────────────────────────────────────────────────
// Driver
// ─────────────────────────────────────────────────────────────
function main() {
    console.log('🧪 test_distributed_lock — Story 6.4 regression suite');
    scenarioHappyPath();
    scenarioCollision();
    scenarioTtlExpiry();
    scenarioStealDetection();
    scenarioLivenessReclaim();
    console.log('');
    console.log('TEST_DISTRIBUTED_LOCK_OK');
}
try {
    main();
}
catch (err) {
    console.error('❌ test_distributed_lock FAILED:', err instanceof Error ? err.message : err);
    process.exit(1);
}
