import { describe, test, expect } from 'bun:test';
import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';

/**
 * Guard the core refactor invariant: importing browse/src/server.ts must NOT
 * auto-start. Before this PR, the module called `start().catch(...)` at module
 * load time, which made the file impossible for embedders (gbrowser phoenix
 * overlay) to import without spawning a daemon. The fix wraps that kickoff in
 * `if (import.meta.main)` so the side effects only run when the module is the
 * process entry point.
 *
 * Approach: spawn a fresh Bun subprocess that imports the module and emits a
 * structured snapshot (initial vs post-import process state). Parent asserts
 * that no listeners were bound, no Bun.serve started, and no SIGINT handlers
 * were registered. The subprocess uses HOME=tmp + GSTACK_HOME=tmp so any
 * accidental state-dir write lands in a place we can verify is empty.
 */
describe('server.ts module import has no auto-start side effects', () => {
  test('importing server.ts does not bind Bun.serve, register signal handlers, or write state', async () => {
    const tmpHome = path.join(os.tmpdir(), `browse-no-sfx-${Date.now()}-${process.pid}`);
    fs.mkdirSync(tmpHome, { recursive: true });
    const tmpGstack = path.join(tmpHome, '.gstack');

    const childScript = `
const sigintBefore = process.listenerCount('SIGINT');
const sigtermBefore = process.listenerCount('SIGTERM');
const uncaughtBefore = process.listenerCount('uncaughtException');

// Snapshot any keys that look like our state path.
const fs = require('fs');
const path = require('path');

await import(${JSON.stringify(path.resolve(import.meta.dir, '../src/server.ts'))});

// After import, sleep a tick so any setTimeout(0)-style init can run.
await new Promise(r => setTimeout(r, 50));

const sigintAfter = process.listenerCount('SIGINT');
const sigtermAfter = process.listenerCount('SIGTERM');
const uncaughtAfter = process.listenerCount('uncaughtException');

// Check that the gstack home directory wasn't populated as a side effect.
// A lone \`.gitignore\` (the state-dir ignore guard, contents "*") is expected
// and is NOT leaked state — ensureStateDir writes it so persisted cookies/logs
// can never be git-committed. Any OTHER entry (browse.json, session-state.json,
// logs) would be a real auto-start write and must still fail the guard.
let gstackPopulated = false;
try {
  const entries = fs.readdirSync(${JSON.stringify(tmpGstack)}).filter(e => e !== '.gitignore');
  gstackPopulated = entries.length > 0;
} catch {
  // Doesn't exist — that's the win we want.
}

console.log(JSON.stringify({
  sigintBefore, sigintAfter,
  sigtermBefore, sigtermAfter,
  uncaughtBefore, uncaughtAfter,
  gstackPopulated,
}));
// Force exit so any background intervals don't keep this child alive
// (the test framework would see a hang otherwise — which itself is a
// signal that side effects DID run).
process.exit(0);
`;

    const proc = Bun.spawn(['bun', '-e', childScript], {
      env: {
        ...process.env,
        HOME: tmpHome,
        GSTACK_HOME: tmpGstack,
        // Empty so the AUTH_TOKEN env path doesn't deterministically set a token.
        AUTH_TOKEN: '',
        // Force a stub state file so resolveConfig() at module load (if it
        // happens) won't crawl the host's real .gstack/.
        BROWSE_STATE_FILE: path.join(tmpGstack, 'browse.json'),
      },
      stdout: 'pipe',
      stderr: 'pipe',
    });

    const stdout = await new Response(proc.stdout).text();
    const stderr = await new Response(proc.stderr).text();
    await proc.exited;

    // The last JSON line in stdout is our snapshot.
    const jsonLine = stdout.trim().split('\n').filter(l => l.startsWith('{')).pop();
    expect(jsonLine, `child stderr: ${stderr}`).toBeDefined();

    const snapshot = JSON.parse(jsonLine!);

    // No new signal handlers registered (gated on import.meta.main, which
    // is false in the subprocess because `bun -e` is the entry point).
    expect(snapshot.sigintAfter).toBe(snapshot.sigintBefore);
    expect(snapshot.sigtermAfter).toBe(snapshot.sigtermBefore);
    expect(snapshot.uncaughtAfter).toBe(snapshot.uncaughtBefore);

    // gstack home should remain empty — initRegistry/initAuditLog/etc. side
    // effects from module load are acceptable (they happen at module level),
    // but only insofar as they don't bind listeners or write project state.
    // The presence/absence test here proves we didn't bind Bun.serve (which
    // would also try to write the state file).
    expect(snapshot.gstackPopulated).toBe(false);

    // Cleanup
    try { fs.rmSync(tmpHome, { recursive: true, force: true }); } catch { /* best effort */ }
  }, 30_000);
});
