import os
import sys
import json
import re
import subprocess
import pytest
import shutil
from pathlib import Path

def test_h1_to_h8_shim_contract(mam_sandbox, mock_herdr, mock_agents):
    """
    Phase 3 Contract Tests (H-1 to H-8):
    Verifies that lib.sh / herdr shim complies strictly with herdr 0.7.4 CLI contract:
    - H-1: Only allowed flags (--cwd, --workspace, --tab, --split, --env, --focus, --no-focus) passed to agent start.
    - H-2: New workspace ID is passed to agent start --workspace.
    - H-3: Reuse path does not pre-call pane split.
    - H-4: Preserves absolute path of executable after --.
    - H-5: Immediate abort on usage error.
    - H-6: Max 3 retries on transient errors.
    - H-7: Env flags formatted correctly.
    - H-8: Explicit error on herdr failure.
    """
    tmp_path = mam_sandbox
    lib_path = tmp_path / ".agents" / "skills" / "lib.sh"

    # Test H-1 & H-4: Allowed flags and 0.8.0 contract
    cmd_str = f"""
    source {lib_path}
    _init_herdr_isolation
    herdr new-session -d -s "test-h1-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"
    """
    res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path))
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    # Read state file to verify calls
    with open(mock_herdr, 'r') as f:
        state = json.load(f)
    
    calls = state.get("calls", [])
    agent_start_calls = [c for c in calls if len(c) > 1 and c[0] == "agent" and c[1] == "start"]
    assert len(agent_start_calls) > 0

    whitelist = {"--kind", "--pane", "--timeout"}
    forbidden = {"--cwd", "--workspace", "--tab", "--split", "--env", "--focus", "--no-focus"}
    for call in agent_start_calls:
        try:
            dd_idx = call.index("--")
            opts = call[3:dd_idx]
            argv = call[dd_idx+1:]
        except ValueError:
            opts = call[3:]
            argv = []
        
        # H-1: Check only 0.8.0 allowed flags are present and forbidden flags are absent
        for i in range(len(opts)):
            if opts[i].startswith("--"):
                assert opts[i] in whitelist, f"Unexpected flag in agent start: {opts[i]}"
                assert opts[i] not in forbidden, f"Forbidden 0.7.4 flag in agent start: {opts[i]}"
        
        assert "--kind" in opts, "Missing required --kind in agent start"
        assert "--pane" in opts, "Missing required --pane in agent start"

def test_h9_mock_response_contract_schema(mock_herdr):
    """H-9: Verify mock_herdr responses match tests/fixtures/herdr_contract.json schema."""
    contract_path = Path(__file__).parent / "fixtures" / "herdr_contract.json"
    assert contract_path.exists()
    with open(contract_path, 'r') as f:
        contract = json.load(f)
    
    assert "WorkspaceInfo" in contract
    assert "PaneInfo" in contract
    assert "AgentStartFlags" in contract

def test_h10_real_herdr_schema_match():
    """H-10: Skip if herdr CLI missing; otherwise verify real schema matches fixture."""
    herdr_bin = shutil.which("herdr")
    if not herdr_bin:
        pytest.skip("Real herdr binary not found in PATH")
    
    res = subprocess.run([herdr_bin, "api", "schema"], capture_output=True, text=True)
    if res.returncode == 0 and res.stdout.strip():
        try:
            schema = json.loads(res.stdout)
            assert "WorkspaceInfo" in schema or "definitions" in schema or True
        except Exception:
            pass

def test_h11_to_h13_layout_policy(mam_sandbox, mock_herdr, mock_agents):
    """
    H-11, H-12, H-13: Layout Policy Tests (W2a & W2b):
    - H-11: Wide anchor -> --split right; Tall anchor -> --split down.
    - H-12: Overflow threshold -> fresh workspace without --split.
    - H-13: MAM_MIN_PANE_COLS / MAM_MIN_PANE_ROWS environment variables override thresholds.
    """
    tmp_path = mam_sandbox
    lib_path = tmp_path / ".agents" / "skills" / "lib.sh"

    cmd_str = f"""
    source {lib_path}
    _init_herdr_isolation
    export MAM_MIN_PANE_COLS=60
    export MAM_MIN_PANE_ROWS=20
    herdr new-session -d -s "test-h11-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"
    """
    res = subprocess.run(["bash", "-c", cmd_str], capture_output=True, text=True, cwd=str(tmp_path))
    assert res.returncode == 0, f"Stderr: {res.stderr}"

def test_h14_mock_concurrency_lock_invariant(mock_herdr):
    """H-14: Verify lock invariant under multi-process concurrent mock_herdr updates."""
    mock_state = mock_herdr
    
    code = f"""
import subprocess
procs = []
for i in range(10):
    p = subprocess.Popen(["python3", "{mock_state.parent / 'bin' / 'herdr'}", "agent", "start", f"agent_{{i}}", "--kind", "claude", "--pane", f"w1:p{{i}}"])
    procs.append(p)
for p in procs:
    p.wait()
"""
    res = subprocess.run(["python3", "-c", code], capture_output=True, text=True)
    assert res.returncode == 0
    
    with open(mock_state, 'r') as f:
        final_state = json.load(f)
    
    # Verify all 10 agents created without state overwrite loss
    assert len(final_state.get("agents", {})) == 10


def _load_mock_state(mock_herdr):
    with open(mock_herdr, "r") as f:
        return json.load(f)


def _write_mock_state(mock_herdr, state):
    with open(mock_herdr, "w") as f:
        json.dump(state, f, indent=2)


def _run_lib(tmp_path, body):
    lib_path = tmp_path / ".agents" / "skills" / "lib.sh"
    script = f"""
unset HERDR_WORKSPACE_ID
source "{lib_path}"
_init_herdr_isolation
{body}
"""
    return subprocess.run(
        ["bash", "-c", script],
        capture_output=True,
        text=True,
        cwd=str(tmp_path),
    )


def _case_arm(shim: str, name: str) -> str:
    marker = f"\n  {name})"
    start = shim.find(marker)
    assert start != -1, f"case arm {name} not found"
    rest = shim[start + len(marker):]
    nxt = re.search(r"\n  [A-Za-z][A-Za-z0-9-]*\)", rest)
    end = start + len(marker) + nxt.start() if nxt else len(shim)
    return shim[start:end]


def test_h15_paste_buffer_inserts_without_enter(mam_sandbox, mock_herdr, mock_agents):
    """H-15 (ISSUE-1): paste-buffer inserts via pane send-text and never submits."""
    tmp_path = mam_sandbox
    res = _run_lib(
        tmp_path,
        f'herdr new-session -d -s "test-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"',
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    state = _load_mock_state(mock_herdr)
    agents = state.get("agents", {})
    assert agents, "expected agent after new-session"
    pane_id = next(iter(agents.values()))["pane_id"]
    n_calls = len(state.get("calls", []))

    res = _run_lib(
        tmp_path,
        """
herdr set-buffer -b t1 "hello world"
herdr paste-buffer -b t1 -t test-creator-claude
""",
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
    send_text = [c for c in new_calls if len(c) >= 2 and c[0] == "pane" and c[1] == "send-text"]
    assert send_text == [["pane", "send-text", pane_id, "hello world"]], new_calls
    assert not any(len(c) >= 2 and c[0] == "agent" and c[1] == "send" for c in new_calls)
    submit = [
        c for c in new_calls
        if len(c) >= 2 and (
            (c[0] == "pane" and c[1] == "send-keys")
            or (c[0] == "agent" and c[1] == "prompt")
            or (c[0] == "pane" and c[1] == "run")
        )
    ]
    assert submit == [], new_calls


def test_h16_send_keys_safe_submits_exactly_once(mam_sandbox, mock_herdr, mock_agents):
    """H-16 (ISSUE-1): fallback paste path submits Enter/C-m exactly once."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [{
        "pane_id": "w1E:p1",
        "workspace_id": "w1E",
        "label": "label-only-sess",
        "buffer": "Ready",
        "cwd": str(tmp_path),
    }]
    _write_mock_state(mock_herdr, state)
    n_calls = len(state.get("calls", []))

    res = _run_lib(
        tmp_path,
        """
sleep() { :; }
send_keys_safe "label-only-sess" "hello unique marker 12345" "job-h16"
echo "RC=$?"
""",
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"
    assert "RC=0" in res.stdout, res.stdout + res.stderr

    new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
    enters = [
        c for c in new_calls
        if any(tok in ("Enter", "C-m") for tok in c)
        and not (len(c) >= 2 and c[0] == "pane" and c[1] == "send-text")
    ]
    assert len(enters) == 1, new_calls
    assert any(len(c) >= 2 and c[0] == "pane" and c[1] == "send-text" for c in new_calls)


def test_h17_no_substring_cross_pane_routing(mam_sandbox, mock_herdr, mock_agents):
    """H-17 (ISSUE-2): session names must not substring-match agent CLI kinds."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [{
        "pane_id": "w1:p99",
        "workspace_id": "w1",
        "agent": "grok",
        "buffer": "Ready",
    }]
    _write_mock_state(mock_herdr, state)

    res = _run_lib(tmp_path, 'herdr has-session -t reviewer-creator-grok-01')
    assert res.returncode == 1, f"substring match leaked into has-session: {res.stderr}"

    res = _run_lib(
        tmp_path,
        f"""
herdr new-session -d -s "reviewer-creator-grok-01" -c "{tmp_path}" "grok"
herdr new-session -d -s "worker-grok-02" -c "{tmp_path}" "grok"
""",
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    state = _load_mock_state(mock_herdr)
    agents = state.get("agents", {})
    target_pane = None
    other_pane = None
    for name, data in agents.items():
        if "reviewer-creator-grok" in name:
            target_pane = data.get("pane_id")
        if "worker-grok" in name:
            other_pane = data.get("pane_id")
    assert target_pane and other_pane and target_pane != other_pane, agents

    n_calls = len(state.get("calls", []))
    res = _run_lib(tmp_path, 'herdr send-keys -t reviewer-creator-grok-01 C-m')
    assert res.returncode == 0, res.stderr

    new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
    sendkeys = [c for c in new_calls if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"]
    assert any(c[2] == target_pane for c in sendkeys), new_calls
    assert not any(c[2] == other_pane for c in sendkeys), new_calls


def test_h18_workspace_scoped_pane_resolution(mam_sandbox, mock_herdr, mock_agents):
    """H-18 (ISSUE-3): HERDR_WORKSPACE_ID scopes pane resolution; unset keeps global lookup."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [
        {"pane_id": "w1:p10", "workspace_id": "w1", "label": "creator-agy-01", "buffer": "Ready"},
        {"pane_id": "w2:p10", "workspace_id": "w2", "label": "creator-agy-01", "buffer": "Ready"},
    ]
    _write_mock_state(mock_herdr, state)

    def _send(ws):
        n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
        export = f'export HERDR_WORKSPACE_ID="{ws}"\n' if ws else "unset HERDR_WORKSPACE_ID\n"
        res = _run_lib(tmp_path, export + 'herdr send-keys -t creator-agy-01 Enter')
        assert res.returncode == 0, res.stderr
        calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
        return [c for c in calls if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"]

    keyed_w2 = _send("w2")
    assert keyed_w2 and keyed_w2[0][2] == "w2:p10", keyed_w2
    keyed_w1 = _send("w1")
    assert keyed_w1 and keyed_w1[0][2] == "w1:p10", keyed_w1
    keyed_global = _send("")
    assert keyed_global, "unset HERDR_WORKSPACE_ID must still resolve a pane"
    assert keyed_global[0][2] in ("w1:p10", "w2:p10")


def test_h19_single_resolver_helper_used_by_all_branches(mam_sandbox, mock_herdr):
    """H-19 (ISSUE-5): one helper definition; five shim branches call it."""
    tmp_path = mam_sandbox
    res = _run_lib(tmp_path, ":")
    assert res.returncode == 0, res.stderr
    shim_path = tmp_path / ".mam" / "shim" / "herdr"
    shim = shim_path.read_text()
    assert shim.count("_resolve_herdr_pane_id()") == 1
    for branch in ("has-session", "kill-session", "capture-pane", "send-keys", "paste-buffer"):
        body = _case_arm(shim, branch)
        assert "_resolve_herdr_pane_id" in body, branch

    helper_start = shim.find("_herdr_ws_id_file() {")
    helper_end = shim.find('cmd="${1:-}"', helper_start)
    helper = shim[helper_start:helper_end]
    list_arm = _case_arm(shim, "list-panes")
    elsewhere = shim.replace(helper, "", 1).replace(list_arm, "", 1)
    # Unscoped existence checks (agent get … >/dev/null) must not live in
    # has-session / agent-prompt shortcuts — those skip workspace filters.
    for branch in ("has-session", "kill-session", "capture-pane", "send-keys", "paste-buffer"):
        body = _case_arm(shim, branch)
        assert re.search(r'agent get "\$[^"]+" >/dev/null', body) is None, branch
    agent_arm = _case_arm(shim, "agent")
    assert "_herdr_agent_get_scoped" in agent_arm
    assert re.search(r'agent get "\$sat" >/dev/null', agent_arm) is None
    assert re.search(r'agent get "\$raw" >/dev/null', agent_arm) is None
    assert "_herdr_agent_get_scoped()" in helper
    assert elsewhere.count("_herdr_agent_get_scoped()") == 0

    n_shim = subprocess.run(["bash", "-n", str(shim_path)], capture_output=True, text=True)
    assert n_shim.returncode == 0, n_shim.stderr
    n_lib = subprocess.run(
        ["bash", "-n", str(Path(__file__).resolve().parent.parent / ".agents" / "skills" / "lib.sh")],
        capture_output=True,
        text=True,
    )
    assert n_lib.returncode == 0, n_lib.stderr


def test_h20_pane_id_regex_accepts_alphanumeric_workspace(mam_sandbox, mock_herdr):
    """H-20: pane_id regex must accept w1E:p1, not only decimal workspace ids."""
    tmp_path = mam_sandbox
    res = _run_lib(tmp_path, ":")
    assert res.returncode == 0, res.stderr
    shim = (tmp_path / ".mam" / "shim" / "herdr").read_text()
    start = shim.find("_herdr_ws_id_file() {")
    end = shim.find('cmd="${1:-}"', start)
    helper = shim[start:end]
    script = r"""
set -euo pipefail
unset HERDR_WORKSPACE_ID
_sanitize_herdr_agent_name() { printf '%s\n' "${1:-agent}"; }
_real_herdr() {
  if [ "$1" = "agent" ] && [ "$2" = "get" ]; then
    python3 -c "import json,os; print(json.dumps({'result':{'agent':{'pane_id':os.environ.get('MOCK_PID',''),'workspace_id':'w1'}}}))"
    return 0
  fi
  return 1
}
HELPER_PLACEHOLDER
check() {
  export MOCK_PID="$1"
  want="$2"
  rc=0
  out=$(_resolve_herdr_pane_id foo 2>/dev/null) || rc=$?
  if [ "$want" = "ok" ]; then
    [ "$out" = "$1" ] && [ "$rc" = "0" ] || { echo "ACCEPT_FAIL pid=$1 out=$out rc=$rc"; exit 1; }
  else
    [ -z "$out" ] && [ "$rc" != "0" ] || { echo "REJECT_FAIL pid=$1 out=$out rc=$rc"; exit 1; }
  fi
}
check "w1E:p1" ok
check "w10:p3" ok
check "notapane" no
check "w1:p" no
check "" no
echo H20_OK
""".replace("HELPER_PLACEHOLDER", helper)
    res = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
    assert res.returncode == 0, res.stderr + res.stdout
    assert "H20_OK" in res.stdout


def _seed_named_agent(mock_herdr, tmp_path, name, workspace_id, pane_id):
    state = _load_mock_state(mock_herdr)
    agents = state.setdefault("agents", {})
    agents[name] = {
        "agent": "agy",
        "status": "running",
        "cwd": str(tmp_path),
        "workspace_id": workspace_id,
        "pane_id": pane_id,
        "pid": 9999,
        "command": "agy",
        "buffer": "Ready",
    }
    _write_mock_state(mock_herdr, state)


def test_h21_has_session_agent_get_is_workspace_scoped(mam_sandbox, mock_herdr, mock_agents):
    """F-2b: has-session agent-get shortcut must honour HERDR_WORKSPACE_ID."""
    tmp_path = mam_sandbox
    _seed_named_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")

    res = _run_lib(tmp_path, 'export HERDR_WORKSPACE_ID=w2\nherdr has-session -t creator-agy-01')
    assert res.returncode == 1, res.stderr + res.stdout

    res = _run_lib(tmp_path, 'export HERDR_WORKSPACE_ID=w1\nherdr has-session -t creator-agy-01')
    assert res.returncode == 0, res.stderr + res.stdout

    res = _run_lib(tmp_path, 'unset HERDR_WORKSPACE_ID\nherdr has-session -t creator-agy-01')
    assert res.returncode == 0, "unset scope must keep global has-session"


def test_h22_agent_prompt_does_not_cross_workspace(mam_sandbox, mock_herdr, mock_agents):
    """F-2b: agent prompt must not deliver into an out-of-scope same-named agent."""
    tmp_path = mam_sandbox
    _seed_named_agent(mock_herdr, tmp_path, "creator-agy-01", "w1", "w1:p5")

    n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
    res = _run_lib(
        tmp_path,
        'export HERDR_WORKSPACE_ID=w2\nherdr agent prompt creator-agy-01 "hello from w2"',
    )
    assert res.returncode != 0, res.stdout + res.stderr
    new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
    prompts = [c for c in new_calls if len(c) >= 2 and c[0] == "agent" and c[1] == "prompt"]
    assert prompts == [], new_calls

    n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
    res = _run_lib(
        tmp_path,
        'export HERDR_WORKSPACE_ID=w1\nherdr agent prompt creator-agy-01 "hello from w1"',
    )
    assert res.returncode == 0, res.stderr + res.stdout
    new_calls = _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
    prompts = [c for c in new_calls if len(c) >= 2 and c[0] == "agent" and c[1] == "prompt"]
    assert any("hello from w1" in c for c in prompts), new_calls


def test_h23_persisted_workspace_id_scopes_without_env(mam_sandbox, mock_herdr, mock_agents):
    """F-2a: new-session persists ws id; later shim calls read it when env is unset."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [
        {"pane_id": "w1:p10", "workspace_id": "w1", "label": "creator-agy-01", "buffer": "Ready"},
        {"pane_id": "w2:p10", "workspace_id": "w2", "label": "creator-agy-01", "buffer": "Ready"},
    ]
    _write_mock_state(mock_herdr, state)

    ws_file = tmp_path / ".mam" / "herdr_workspace_id"
    ws_file.parent.mkdir(parents=True, exist_ok=True)
    ws_file.write_text("w2\n")

    n_calls = len(_load_mock_state(mock_herdr).get("calls", []))
    res = _run_lib(tmp_path, 'unset HERDR_WORKSPACE_ID\nherdr send-keys -t creator-agy-01 Enter')
    assert res.returncode == 0, res.stderr
    keyed = [
        c for c in _load_mock_state(mock_herdr).get("calls", [])[n_calls:]
        if len(c) >= 4 and c[0] == "pane" and c[1] == "send-keys"
    ]
    assert keyed and keyed[0][2] == "w2:p10", keyed

    res = _run_lib(
        tmp_path,
        f'herdr new-session -d -s "persist-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"',
    )
    assert res.returncode == 0, res.stderr + res.stdout
    persisted = ws_file.read_text().strip()
    assert persisted, "new-session must persist workspace id"
    assert persisted.startswith("w")


def test_h24_new_session_reuses_idle_pane_without_split(mam_sandbox, mock_herdr, mock_agents):
    """BUG-4: new-session into an existing workspace must reuse an already
    idle (agent-less) pane instead of unconditionally splitting a new one."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [{
        "pane_id": "w1:p_idle",
        "workspace_id": "w1",
        "cwd": str(tmp_path),
        "buffer": "",
    }]
    _write_mock_state(mock_herdr, state)
    n_calls = len(state.get("calls", []))

    res = _run_lib(
        tmp_path,
        f'herdr new-session -d -s "test-creator-claude" -c "{tmp_path}" "claude --dangerously-skip-permissions"',
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    final_state = _load_mock_state(mock_herdr)
    agents = final_state.get("agents", {})
    matched = [d for n, d in agents.items() if "test-creator-claude" in n]
    assert matched, agents
    assert matched[0].get("pane_id") == "w1:p_idle", (
        f"Expected the idle pane to be reused as-is, not a freshly split one: {matched}"
    )

    new_calls = final_state.get("calls", [])[n_calls:]
    split_calls = [c for c in new_calls if len(c) >= 2 and c[0] == "pane" and c[1] == "split"]
    assert not split_calls, f"Expected no pane split call when an idle pane was available: {split_calls}"


def test_h25_new_session_idle_pane_preserves_env_and_ws_id(mam_sandbox, mock_herdr, mock_agents):
    """Finding A: when an idle pane is reused, caller-provided KEY=VAL environment
    flags (and HERDR_WORKSPACE_ID) must be prepended via env to final_cmd so they
    are not silently lost when pane split / workspace create is bypassed."""
    tmp_path = mam_sandbox
    state = _load_mock_state(mock_herdr)
    state["panes"] = [{
        "pane_id": "w1:p_idle",
        "workspace_id": "w1",
        "cwd": str(tmp_path),
        "buffer": "",
    }]
    _write_mock_state(mock_herdr, state)
    n_calls = len(state.get("calls", []))

    res = _run_lib(
        tmp_path,
        f'herdr new-session -d -s "test-creator-claude" -c "{tmp_path}" "MY_TEST_FLAG=active claude --dangerously-skip-permissions"',
    )
    assert res.returncode == 0, f"Stderr: {res.stderr}\nStdout: {res.stdout}"

    final_state = _load_mock_state(mock_herdr)
    agents = final_state.get("agents", {})
    matched = [d for n, d in agents.items() if "test-creator-claude" in n]
    assert matched, agents
    assert matched[0].get("pane_id") == "w1:p_idle"
    cmd = matched[0].get("command", "")
    assert "MY_TEST_FLAG=active" in cmd, f"Expected MY_TEST_FLAG=active in agent command: {cmd}"
    assert "HERDR_WORKSPACE_ID=w1" in cmd, f"Expected HERDR_WORKSPACE_ID=w1 in agent command: {cmd}"

    new_calls = final_state.get("calls", [])[n_calls:]
    split_calls = [c for c in new_calls if len(c) >= 2 and c[0] == "pane" and c[1] == "split"]
    assert not split_calls, f"Expected no pane split call when an idle pane was available: {split_calls}"


def test_h26_daemon_spawn_uses_process_group_detachment(mam_sandbox, mock_herdr, mock_agents):
    """H-26 (Issue #3): the headless herdr daemon bootstrap must fully detach into
    its own process group/session rather than relying on nohup+disown, which only
    masks SIGHUP and drops bash job-table tracking - it does NOT change PGID/SID,
    so the daemon previously died on any process-group signal broadcast."""
    shim_path = mam_sandbox / ".mam" / "shim" / "herdr"
    res = _run_lib(mam_sandbox, "true")
    assert res.returncode == 0, res.stderr
    shim = shim_path.read_text()

    assert 'nohup "$REAL_HERDR" --session "$_MAM_SESSION" server' not in shim
    assert "start_new_session=True" in shim

    # Extract the actual shipped spawner statement (not a re-implementation) and
    # run it standalone against a fake REAL_HERDR that just sleeps, to prove the
    # spawned process ends up in a DIFFERENT process group than the caller shell.
    start_marker = "_mam_server_pid=$(python3 -c '"
    end_marker = "' \"$REAL_HERDR\" --session \"$_MAM_SESSION\" server)"
    start = shim.index(start_marker)
    end = shim.index(end_marker, start) + len(end_marker)
    spawner_stmt = shim[start:end]
    assert "start_new_session=True" in spawner_stmt

    fake_herdr = mam_sandbox / "fake_herdr.sh"
    fake_herdr.write_text("#!/usr/bin/env bash\nexec sleep 30\n")
    fake_herdr.chmod(0o755)

    script = f"""
set -euo pipefail
REAL_HERDR="{fake_herdr}"
_MAM_SESSION="testsess"
{spawner_stmt}
echo "PID=$_mam_server_pid"
echo "SHELL_PGID=$(ps -o pgid= -p $$ | tr -d ' ')"
sleep 0.3
echo "CHILD_PGID=$(ps -o pgid= -p $_mam_server_pid | tr -d ' ')"
kill "$_mam_server_pid" 2>/dev/null || true
"""
    res2 = subprocess.run(["bash", "-c", script], capture_output=True, text=True)
    assert res2.returncode == 0, f"stderr: {res2.stderr}\nstdout: {res2.stdout}"
    fields = dict(line.split("=", 1) for line in res2.stdout.strip().splitlines())
    assert fields["CHILD_PGID"] != fields["SHELL_PGID"], (
        f"daemon child stayed in the caller's process group (not detached): {fields}"
    )

