/**
 * Tests the split-engine SKIP semantics in bin/gstack-gbrain-sync.ts (plan D12).
 *
 * When localEngineStatus() returns anything except 'ok', the orchestrator's
 * code + memory stages return ran=false summaries; the brain-sync stage runs
 * unchanged. This is the behavior that matters most for Garry's broken-db
 * machine — instead of crashing two stages with ERR output, the orchestrator
 * surfaces a clear skip reason and still pushes artifacts.
 *
 * We test via the script (spawn) rather than importing runCodeImport/runMemoryIngest
 * directly because they're internal to the orchestrator. The fake gbrain
 * binary controls localEngineStatus()'s output.
 */

import { describe, it, expect } from "bun:test";
import {
  mkdtempSync,
  mkdirSync,
  writeFileSync,
  chmodSync,
  rmSync,
} from "fs";
import { tmpdir } from "os";
import { join } from "path";
import { execFileSync, spawnSync } from "child_process";

const SCRIPT = join(import.meta.dir, "..", "bin", "gstack-gbrain-sync.ts");
const BUN_BIN = execFileSync("sh", ["-c", "command -v bun"], { encoding: "utf-8" }).trim();

interface FakeEnv {
  tmp: string;
  bindir: string;
  home: string;
  gstackHome: string;
  cleanup: () => void;
}

/**
 * Build a sandboxed HOME with optional fake gbrain on PATH.
 * `gbrainBehavior` controls how `gbrain sources list` reacts; this drives
 * localEngineStatus()'s output.
 */
function makeEnv(opts: {
  withGbrain: boolean;
  gbrainBehavior?: "ok" | "broken-db" | "broken-config" | "slow";
  withConfig: boolean;
}): FakeEnv {
  const tmp = mkdtempSync(join(tmpdir(), "gbrain-sync-skip-"));
  const bindir = join(tmp, "bin");
  const home = join(tmp, "home");
  const gstackHome = join(home, ".gstack");
  const gbrainDir = join(home, ".gbrain");

  mkdirSync(bindir, { recursive: true });
  mkdirSync(home, { recursive: true });
  mkdirSync(gstackHome, { recursive: true });
  mkdirSync(gbrainDir, { recursive: true });

  if (opts.withConfig) {
    writeFileSync(
      join(gbrainDir, "config.json"),
      JSON.stringify({ engine: "pglite", database_url: "pglite:///fake" }),
    );
  }

  if (opts.withGbrain) {
    const behavior = opts.gbrainBehavior || "ok";
    // "slow": healthy engine, cold pooler connection (#1964) — sleeps past the
    // (test-lowered) probe timeout on `sources list`, then answers fine.
    const sourcesBlock =
      behavior === "slow"
        ? `  sleep 2
  echo '{"sources":[]}'
  exit 0`
        : behavior === "ok"
          ? `  echo '{"sources":[]}'
  exit 0`
          : `  ${
              behavior === "broken-db"
                ? 'echo "Cannot connect to database: . Fix: Check your connection URL in ~/.gbrain/config.json" >&2'
                : 'echo "Error: malformed config.json" >&2'
            }
  exit 1`;
    const fake = `#!/bin/sh
if [ "$1" = "--version" ]; then echo "gbrain 0.33.1.0"; exit 0; fi
if [ "$1 $2" = "sources list" ]; then
${sourcesBlock}
fi
if [ "$1" = "--help" ]; then echo "  import"; exit 0; fi
exit 0
`;
    writeFileSync(join(bindir, "gbrain"), fake);
    chmodSync(join(bindir, "gbrain"), 0o755);
  }

  return {
    tmp,
    bindir,
    home,
    gstackHome,
    cleanup: () => rmSync(tmp, { recursive: true, force: true }),
  };
}

function runOrchestrator(
  env: FakeEnv,
  args: string[],
  extraEnv: Record<string, string> = {},
): { stdout: string; stderr: string; exitCode: number } {
  // Initialize a git repo in the sandbox so repoRoot() finds it (otherwise
  // code stage skips with "not in git repo" before our check ever fires).
  spawnSync("git", ["init", "-q", env.home], { encoding: "utf-8" });
  spawnSync("git", ["-C", env.home, "commit", "--allow-empty", "-m", "init", "-q"], {
    encoding: "utf-8",
    env: { ...process.env, GIT_AUTHOR_NAME: "T", GIT_AUTHOR_EMAIL: "t@t", GIT_COMMITTER_NAME: "T", GIT_COMMITTER_EMAIL: "t@t" },
  });

  const result = spawnSync(BUN_BIN, [SCRIPT, ...args], {
    encoding: "utf-8",
    timeout: 30_000,
    cwd: env.home,
    env: {
      ...process.env,
      HOME: env.home,
      GSTACK_HOME: env.gstackHome,
      PATH: `${env.bindir}:/usr/bin:/bin`,
      ...extraEnv,
    },
  });
  return {
    stdout: result.stdout || "",
    stderr: result.stderr || "",
    exitCode: result.status ?? 1,
  };
}

describe("gstack-gbrain-sync — split-engine SKIP (plan D12)", () => {
  it("PROCEEDS (with warning) when the engine probe times out — slow is not broken (#1964)", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
    try {
      const r = runOrchestrator(env, ["--code-only"], {
        GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300",
      });
      const out = r.stdout + r.stderr;
      // The stage must NOT be skipped with the local-engine reason...
      expect(out).not.toContain("local engine timeout");
      expect(out).not.toContain("config.json is malformed");
      // ...and the proceed-with-warning line must name the env knob.
      expect(out).toContain("GSTACK_GBRAIN_PROBE_TIMEOUT_MS");
    } finally {
      env.cleanup();
    }
  }, 30_000); // proceeding runs the real code-import path against the slow fake (~11s)

  it("memory stage also PROCEEDS (with warning) on probe timeout (#1964)", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
    try {
      const r = runOrchestrator(env, ["--no-code", "--no-brain-sync"], {
        GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300",
      });
      const out = r.stdout + r.stderr;
      expect(out).not.toContain("local engine timeout");
      expect(out).toContain("memory: engine probe timed out");
    } finally {
      env.cleanup();
    }
  }, 30_000);

  it("dream stage also PROCEEDS (with warning) on probe timeout (#1964)", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "slow", withConfig: true });
    try {
      const r = runOrchestrator(
        env,
        ["--dream", "--no-code", "--no-memory", "--no-brain-sync"],
        { GSTACK_GBRAIN_PROBE_TIMEOUT_MS: "300" },
      );
      const out = r.stdout + r.stderr;
      expect(out).not.toContain("local engine timeout");
      expect(out).toContain("dream: engine probe timed out");
    } finally {
      env.cleanup();
    }
  }, 30_000);

  it("SKIPs code stage when local engine is broken-db; brain-sync still attempted", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-db", withConfig: true });
    try {
      const r = runOrchestrator(env, ["--code-only"]);
      // Code stage should be SKIPped with a clear local-engine status reason.
      // Match on the summary substring our skipStageForLocalStatus helper emits.
      expect(r.stdout + r.stderr).toContain("local engine broken-db");
      // Crucial: NOT the legacy "source registration failed" error path that
      // existed before this fix (codex #2 STOP-vs-SKIP consistency).
      expect(r.stdout + r.stderr).not.toContain("source registration failed");
    } finally {
      env.cleanup();
    }
  });

  it("SKIPs memory stage when local engine is broken-config", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "broken-config", withConfig: true });
    try {
      const r = runOrchestrator(env, ["--no-code", "--no-brain-sync"]);
      expect(r.stdout + r.stderr).toContain("local engine broken-config");
    } finally {
      env.cleanup();
    }
  });

  it("SKIPs code stage when gbrain CLI is missing (no-cli)", () => {
    const env = makeEnv({ withGbrain: false, withConfig: false });
    try {
      const r = runOrchestrator(env, ["--code-only"]);
      // Either "no-cli" (from skipStageForLocalStatus) OR the earlier
      // gbrainAvailable() check (which fires first when the CLI is absent —
      // returns "skipped (gbrain CLI not in PATH)"). Both are acceptable for
      // this case; the user-visible outcome is the same.
      const out = r.stdout + r.stderr;
      const hasSkipReason =
        out.includes("no-cli") || out.includes("gbrain CLI not in PATH");
      expect(hasSkipReason).toBe(true);
    } finally {
      env.cleanup();
    }
  });

  it("SKIPs code stage when config is missing (missing-config)", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: false });
    try {
      const r = runOrchestrator(env, ["--code-only"]);
      expect(r.stdout + r.stderr).toContain("local engine missing-config");
    } finally {
      env.cleanup();
    }
  });

  it("runs code stage normally when local engine is ok", () => {
    const env = makeEnv({ withGbrain: true, gbrainBehavior: "ok", withConfig: true });
    try {
      const r = runOrchestrator(env, ["--code-only"]);
      // When ok, the SKIP-for-local-status branch must NOT fire.
      expect(r.stdout + r.stderr).not.toContain("local engine ok");
      expect(r.stdout + r.stderr).not.toContain("local engine no-cli");
      expect(r.stdout + r.stderr).not.toContain("local engine broken-db");
      expect(r.stdout + r.stderr).not.toContain("local engine missing-config");
    } finally {
      env.cleanup();
    }
  });
});
