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

// v1.44 terminal-agent watchdog — static-grep invariants.
//
// The watchdog respawns terminal-agent when its PID dies. Live process-tree
// tests would require spawning, killing, and observing across two real Bun
// processes — slow and flaky in the free tier. These tripwires defend the
// load-bearing properties: identity-based liveness check (not name match),
// crash-loop guard, gated on ownsTerminalAgent, and cleared on shutdown.

const SERVER_TS = path.resolve(import.meta.path, '..', '..', 'src', 'server.ts');
const CONTROL_TS = path.resolve(import.meta.path, '..', '..', 'src', 'terminal-agent-control.ts');

describe('terminal-agent watchdog (v1.44+)', () => {
  test('1. spawnTerminalAgent helper exists with PID return type', () => {
    const src = fs.readFileSync(CONTROL_TS, 'utf-8');
    expect(src).toMatch(/export function spawnTerminalAgent\(/);
    // Must clean up prior PID before spawning (no zombies).
    expect(src).toContain('readAgentRecord(stateDir)');
    expect(src).toContain('killAgentByRecord(prior');
    expect(src).toContain('clearAgentRecord(stateDir)');
  });

  test('2. watchdog is gated on ownsTerminalAgent', () => {
    const src = fs.readFileSync(SERVER_TS, 'utf-8');
    // Match the comment + the guard. The guard MUST be a positive check;
    // an inverted check would respawn for embedders and trample their PTY.
    const block = sliceBetween(src, '─── Terminal-Agent Watchdog', 'Factory-scoped validateAuth');
    expect(block).toMatch(/if \(ownsTerminalAgent\)/);
    expect(block).toContain('agentWatchdogInterval = setInterval');
  });

  test('3. watchdog uses PID liveness, not process name probe', () => {
    const src = fs.readFileSync(SERVER_TS, 'utf-8');
    const block = sliceBetween(src, '─── Terminal-Agent Watchdog', 'Factory-scoped validateAuth');
    // The whole point of the v1.44 watchdog over v1.43- pkill teardown:
    // identity-based liveness. Slow-but-alive agents must NOT trigger
    // respawn (split-brain defense).
    expect(block).toContain('readAgentRecord(stateDir)');
    expect(block).toContain('isProcessAlive(record.pid)');
    // Negative: no executable name-based process lookup. Allow the strings
    // to appear in prose comments (the watchdog doc explains what it
    // replaces), reject only actual invocations.
    expect(block).not.toMatch(/spawnSync\s*\(\s*['"]pkill/);
    expect(block).not.toMatch(/Bun\.spawn\s*\(\s*\[\s*['"]pgrep/);
  });

  test('4. crash-loop guard with rolling window', () => {
    const src = fs.readFileSync(SERVER_TS, 'utf-8');
    const block = sliceBetween(src, '─── Terminal-Agent Watchdog', 'Factory-scoped validateAuth');
    // The window MUST be derived from the tick, not a fixed 60_000. It was
    // hardcoded to 60_000 against a 60_000ms tick, so at most ONE respawn
    // could ever sit inside the window and the `>= RESPAWN_GUARD_MAX` trip
    // was unreachable — a steady one-respawn-per-tick leak ran unbounded
    // instead of self-limiting after 3. Pinning the literal is what let that
    // ship, so pin the relationship instead.
    expect(block).toMatch(/RESPAWN_GUARD_WINDOW_MS =[\s\S]{0,200}AGENT_WATCHDOG_TICK_MS/);
    expect(block).toContain('RESPAWN_GUARD_MAX = 3');
    expect(block).toContain('respawnHistory');
    expect(block).toContain('agentRespawnGuardTripped');
    // Window pruning: old entries must be evicted before counting toward
    // the limit. Otherwise a daemon up for a week with one crash a day
    // would eventually trip the guard.
    expect(block).toMatch(/respawnHistory\.shift\(\)/);
  });

  test('5. watchdog interval is cleared on shutdown', () => {
    const src = fs.readFileSync(SERVER_TS, 'utf-8');
    expect(src).toContain('if (agentWatchdogInterval) clearInterval(agentWatchdogInterval)');
  });

  test('6. tick interval is env-overridable for tests', () => {
    const src = fs.readFileSync(SERVER_TS, 'utf-8');
    expect(src).toContain('GSTACK_AGENT_WATCHDOG_TICK_MS');
  });

  test('7. CLI cold-start path uses the same spawnTerminalAgent helper', () => {
    const cli = fs.readFileSync(
      path.resolve(import.meta.path, '..', '..', 'src', 'cli.ts'),
      'utf-8',
    );
    // Otherwise the CLI and watchdog could drift on spawn env/cwd, and
    // teardown invariants tested against one would silently miss the other.
    expect(cli).toContain('spawnTerminalAgent({');
    expect(cli).toContain("from './terminal-agent-control'");
  });
});

function sliceBetween(source: string, start: string, end: string): string {
  const i = source.indexOf(start);
  if (i === -1) throw new Error(`marker not found: ${start}`);
  const j = source.indexOf(end, i + start.length);
  if (j === -1) throw new Error(`end marker not found: ${end}`);
  return source.slice(i, j);
}
