import os
import subprocess
import sys
import threading
import time
from pathlib import Path

import pytest

from hermes_cli import active_sessions



def _backdate_leases(*homes, age_seconds=600.0):
    """Age every lease in the given registries past the self-orphan grace."""
    for home in homes:
        state_path = active_sessions._state_path(home)
        entries = active_sessions._read_entries(state_path)
        for entry in entries:
            entry["started_at"] = time.time() - age_seconds
        active_sessions._write_entries(state_path, entries)


def test_resolve_max_concurrent_sessions_values():
    assert active_sessions.resolve_max_concurrent_sessions({}) is None
    assert active_sessions.resolve_max_concurrent_sessions({"max_concurrent_sessions": None}) is None
    assert active_sessions.resolve_max_concurrent_sessions({"max_concurrent_sessions": 0}) is None
    assert active_sessions.resolve_max_concurrent_sessions({"max_concurrent_sessions": -1}) is None
    assert active_sessions.resolve_max_concurrent_sessions({"max_concurrent_sessions": "3"}) == 3
    assert (
        active_sessions.resolve_max_concurrent_sessions(
            {"gateway": {"max_concurrent_sessions": 4}}
        )
        == 4
    )
    assert (
        active_sessions.resolve_max_concurrent_sessions(
            {"max_concurrent_sessions": 2, "gateway": {"max_concurrent_sessions": 4}}
        )
        == 2
    )

    assert active_sessions.resolve_max_concurrent_sessions({"max_concurrent_sessions": "many"}) is None












def test_cross_process_acquire_claims_only_one_last_slot(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    repo_root = Path(__file__).resolve().parents[2]
    ready_dir = tmp_path / "ready"
    ready_dir.mkdir()
    results_dir = tmp_path / "results"
    results_dir.mkdir()
    go_file = tmp_path / "go"
    env = os.environ.copy()
    env["HERMES_HOME"] = str(home)
    env["PYTHONPATH"] = str(repo_root)
    script = (
        "import os, time\n"
        "from pathlib import Path\n"
        "from hermes_cli.active_sessions import try_acquire_active_session\n"
        "idx = os.environ['WORKER_INDEX']\n"
        "worker_count = int(os.environ['WORKER_COUNT'])\n"
        "delayed_worker = os.environ.get('DELAYED_WORKER_INDEX')\n"
        "ready_dir = Path(os.environ['READY_DIR'])\n"
        "results_dir = Path(os.environ['RESULTS_DIR'])\n"
        "go_file = Path(os.environ['GO_FILE'])\n"
        "(ready_dir / idx).write_text('ready', encoding='utf-8')\n"
        "deadline = time.time() + 10\n"
        "while not go_file.exists():\n"
        "    if time.time() > deadline:\n"
        "        raise RuntimeError('timed out waiting for go file')\n"
        "    time.sleep(0.01)\n"
        "if idx == delayed_worker:\n"
        "    time.sleep(2.5)\n"
        "lease, message = try_acquire_active_session(\n"
        "    session_id=f'process-{idx}',\n"
        "    surface='cli',\n"
        "    config={'max_concurrent_sessions': 1},\n"
        ")\n"
        "if lease is None:\n"
        "    (results_dir / idx).write_text('BLOCK', encoding='utf-8')\n"
        "    print('BLOCK', flush=True)\n"
        "else:\n"
        "    (results_dir / idx).write_text('OK', encoding='utf-8')\n"
        "    print('OK', flush=True)\n"
        "    deadline = time.time() + 10\n"
        "    while len(list(results_dir.iterdir())) < worker_count:\n"
        "        if time.time() > deadline:\n"
        "            raise RuntimeError('timed out waiting for all workers to attempt acquire')\n"
        "        time.sleep(0.01)\n"
        "    lease.release()\n"
    )
    workers: list[subprocess.Popen[str]] = []
    try:
        for index in range(6):
            worker_env = env.copy()
            worker_env["WORKER_INDEX"] = str(index)
            worker_env["WORKER_COUNT"] = "6"
            worker_env["DELAYED_WORKER_INDEX"] = "5"
            worker_env["READY_DIR"] = str(ready_dir)
            worker_env["RESULTS_DIR"] = str(results_dir)
            worker_env["GO_FILE"] = str(go_file)
            workers.append(
                subprocess.Popen(
                    [sys.executable, "-c", script],
                    env=worker_env,
                    text=True,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                )
            )

        deadline = time.time() + 10
        while len(list(ready_dir.iterdir())) < len(workers):
            if time.time() > deadline:
                raise AssertionError("workers did not become ready")
            time.sleep(0.01)
        go_file.write_text("go", encoding="utf-8")

        outputs = []
        for worker in workers:
            stdout, stderr = worker.communicate(timeout=10)
            assert worker.returncode == 0, stderr
            outputs.append(stdout.strip())
    finally:
        for worker in workers:
            if worker.poll() is None:
                worker.kill()
                worker.communicate()

    assert outputs.count("OK") == 1
    assert outputs.count("BLOCK") == len(workers) - 1
    assert active_sessions.active_session_registry_snapshot() == []




def test_release_orphaned_leases_reclaims_only_unowned_own_pid_entries(tmp_path, monkeypatch):
    """A long-lived server must reclaim leases whose session skipped teardown.

    ``_prune_dead`` only fires when the owning pid dies, so a ``hermes
    dashboard`` running for days holds a leaked lease until restart. The
    process reconciles against the leases it still owns instead.
    """
    monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
    cfg = {"max_concurrent_sessions": 5}
    kept, orphan = (
        active_sessions.try_acquire_active_session(
            session_id=sid, surface="desktop", config=cfg
        )[0]
        for sid in ("kept", "orphaned")
    )
    # Another live process's lease is not ours to reclaim.
    active_sessions._write_entries(
        active_sessions._state_path(),
        active_sessions._read_entries(active_sessions._state_path())
        + [{"lease_id": "elsewhere", "session_id": "other", "surface": "cli", "pid": os.getpid() }],
    )

    _backdate_leases(tmp_path / ".hermes")
    assert active_sessions.release_orphaned_leases({kept.lease_id, "elsewhere"}) == 1
    assert sorted(
        entry["session_id"]
        for entry in active_sessions.active_session_registry_snapshot()
    ) == ["kept", "other"]
    assert orphan is not None


def test_release_orphaned_leases_sweeps_profile_runtime_registries(
    tmp_path, monkeypatch
):
    root = tmp_path / "hermes"
    profile = root / "profiles" / "worker"
    profile.mkdir(parents=True)
    (profile / "config.yaml").write_text("{}\n")  # identity marker: a bare dir is not a profile
    monkeypatch.setenv("HERMES_HOME", str(root))

    root_lease, root_error = active_sessions.try_acquire_active_session(
        session_id="root-orphan", surface="desktop", config={}, registry_home=root
    )
    profile_lease, profile_error = active_sessions.try_acquire_active_session(
        session_id="profile-orphan",
        surface="desktop",
        config={},
        registry_home=profile,
    )
    assert root_lease is not None and root_error is None
    assert profile_lease is not None and profile_error is None

    # A lease written seconds ago is never an orphan: a sibling finalize that
    # snapshotted its live ids before this acquire must not reap it (#101415).
    assert active_sessions.release_orphaned_leases(set()) == 0
    _backdate_leases(root, profile)
    assert active_sessions.release_orphaned_leases(set()) == 2
    assert active_sessions.active_session_registry_snapshot(root) == []
    assert active_sessions.active_session_registry_snapshot(profile) == []


def test_drop_self_orphans_spares_foreign_and_vouched_leases():
    own = os.getpid()
    entries = [
        {"lease_id": "orphan", "pid": own},
        {"lease_id": "live", "pid": own},
        {"lease_id": "foreign", "pid": own + 1},
    ]

    assert active_sessions._drop_self_orphans(entries, None) == entries
    assert active_sessions._drop_self_orphans(entries, {"live"}) == entries[1:]


def test_release_under_profile_home_override_targets_acquisition_registry(
    tmp_path, monkeypatch
):
    """Regression for #85431: a lease acquired against the root HERMES_HOME
    must release from the root registry even when ``release()`` runs inside a
    profile home override (native multiplex runs agent cleanup under
    ``_profile_runtime_scope``). Before the fix the root entry survived and
    the session cap filled with phantom leases."""
    from hermes_constants import (
        reset_hermes_home_override,
        set_hermes_home_override,
    )

    root = tmp_path / "hermes"
    profile = root / "profiles" / "worker"
    profile.mkdir(parents=True)
    (profile / "config.yaml").write_text("{}\n")  # identity marker: a bare dir is not a profile
    monkeypatch.setenv("HERMES_HOME", str(root))

    lease, error = active_sessions.try_acquire_active_session(
        session_id="agent:worker:telegram:dm:synthetic",
        surface="gateway:telegram",
        config={"max_concurrent_sessions": 2},
    )
    assert lease is not None and error is None
    root_registry = root / "runtime" / "active_sessions.json"
    assert root_registry.exists()

    token = set_hermes_home_override(str(profile))
    try:
        lease.release()
    finally:
        reset_hermes_home_override(token)

    assert lease.released is True
    remaining = active_sessions._read_entries(root_registry)
    assert remaining == []
    # No phantom registry created under the profile home.
    assert not (profile / "runtime" / "active_sessions.json").exists()


def test_transfer_under_profile_home_override_targets_acquisition_registry(
    tmp_path, monkeypatch
):
    """Sibling site of #85431: transfer must also update the registry the
    lease was acquired against, not one resolved from the current override."""
    from hermes_constants import (
        reset_hermes_home_override,
        set_hermes_home_override,
    )

    root = tmp_path / "hermes"
    profile = root / "profiles" / "worker"
    profile.mkdir(parents=True)
    (profile / "config.yaml").write_text("{}\n")  # identity marker: a bare dir is not a profile
    monkeypatch.setenv("HERMES_HOME", str(root))

    lease, error = active_sessions.try_acquire_active_session(
        session_id="before",
        surface="gateway:telegram",
        config={"max_concurrent_sessions": 2},
    )
    assert lease is not None and error is None

    token = set_hermes_home_override(str(profile))
    try:
        assert active_sessions.transfer_active_session(lease, session_id="after")
    finally:
        reset_hermes_home_override(token)

    root_registry = root / "runtime" / "active_sessions.json"
    entries = active_sessions._read_entries(root_registry)
    assert [entry["session_id"] for entry in entries] == ["after"]


def test_liveness_registry_corruption_fails_closed_without_overwrite(
    tmp_path, monkeypatch
):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    state_path.parent.mkdir(parents=True)
    corrupt = "{not-json"
    state_path.write_text(corrupt, encoding="utf-8")

    with pytest.raises(active_sessions.ActiveSessionRegistryError):
        with active_sessions.active_session_liveness_guard("session-1"):
            pass

    with pytest.raises(active_sessions.ActiveSessionRegistryError):
        active_sessions.active_session_registry_snapshot()

    assert state_path.read_text(encoding="utf-8") == corrupt

    with pytest.raises(active_sessions.ActiveSessionRegistryError):
        active_sessions.try_acquire_active_session(
            session_id="desktop-1",
            surface="desktop",
            config={},
            track_liveness=True,
        )
    assert state_path.read_text(encoding="utf-8") == corrupt

    # Ownership uncertainty fails CLOSED on every path now (#94595): a corrupt
    # registry must refuse the session — with a typed reason — rather than
    # silently readmitting a possible second writer. It still must not erase
    # the evidence.
    lease, message = active_sessions.try_acquire_active_session(
        session_id="cli-1",
        surface="cli",
        config={"max_concurrent_sessions": 1},
    )
    assert lease is None
    assert getattr(message, "reason", None) == (
        active_sessions.SESSION_COORDINATION_UNAVAILABLE
    )
    assert state_path.read_text(encoding="utf-8") == corrupt


def test_strict_registry_rejects_structurally_invalid_entries(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    base = {
        "lease_id": "lease-1",
        "session_id": "session-1",
        "surface": "desktop",
        "pid": os.getpid(),
        "track_liveness": True,
    }
    invalid_entries = (
        {key: value for key, value in base.items() if key != "lease_id"},
        {**base, "lease_id": ""},
        {key: value for key, value in base.items() if key != "session_id"},
        {**base, "session_id": "  "},
        {**base, "pid": 0},
        {**base, "pid": 1.5},
        {**base, "surface": 1},
        {**base, "track_liveness": "yes"},
        {**base, "metadata": []},
        {**base, "process_start_time": "not-a-number"},
        {**base, "process_start_time": "nan"},
    )

    for entry in invalid_entries:
        active_sessions._write_entries(state_path, [entry])
        original = state_path.read_text(encoding="utf-8")
        with pytest.raises(active_sessions.ActiveSessionRegistryError):
            with active_sessions.active_session_liveness_guard("session-1"):
                pass
        assert state_path.read_text(encoding="utf-8") == original


@pytest.mark.parametrize(
    "second_session_id",
    ("session-a", "session-b"),
    ids=("exact-duplicate", "conflicting-duplicate"),
)
def test_strict_registry_rejects_duplicate_lease_ids(
    tmp_path, monkeypatch, second_session_id
):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    active_sessions._write_entries(
        state_path,
        [
            {
                "lease_id": "duplicate-lease",
                "session_id": "session-a",
                "surface": "desktop",
                "pid": os.getpid(),
                "track_liveness": True,
            },
            {
                "lease_id": "duplicate-lease",
                "session_id": second_session_id,
                "surface": "desktop",
                "pid": os.getpid(),
                "track_liveness": True,
            },
        ],
    )
    original = state_path.read_text(encoding="utf-8")

    with pytest.raises(active_sessions.ActiveSessionRegistryError):
        active_sessions.active_session_registry_snapshot()

    assert state_path.read_text(encoding="utf-8") == original


def test_cap_transfer_does_not_overwrite_registry_corruption(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    lease, message = active_sessions.try_acquire_active_session(
        session_id="cli-old",
        surface="cli",
        config={"max_concurrent_sessions": 1},
    )
    assert lease is not None and message is None

    corrupt = "{not-json"
    state_path.write_text(corrupt, encoding="utf-8")
    assert not active_sessions.transfer_active_session(
        lease,
        session_id="cli-new",
    )
    assert lease.session_id == "cli-old"
    assert state_path.read_text(encoding="utf-8") == corrupt

    lease.release()
    assert lease.released is True
    assert state_path.read_text(encoding="utf-8") == corrupt


def test_liveness_guard_rejects_unknown_pid_state(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    active_sessions._write_entries(
        state_path,
        [
            {
                "lease_id": "unknown-owner",
                "session_id": "session-1",
                "surface": "desktop",
                "pid": 12345,
                "track_liveness": True,
            }
        ],
    )
    monkeypatch.setattr(
        "gateway.status._pid_exists",
        lambda _pid: (_ for _ in ()).throw(OSError("pid lookup unavailable")),
    )

    with pytest.raises(active_sessions.ActiveSessionRegistryError):
        with active_sessions.active_session_liveness_guard("session-1"):
            pass

    # The unknown owner is unrelated to a NEW session id: it stays in the live
    # set (fail closed on capacity, #94595) but no longer refuses the claim
    # outright (#113683).
    lease, message = active_sessions.try_acquire_active_session(
        session_id="cli-cap-session",
        surface="cli",
        config={"max_concurrent_sessions": 1},
    )
    assert lease is None
    assert getattr(message, "reason", None) == active_sessions.MAX_CONCURRENT_SESSIONS
    assert "unknown-owner" in state_path.read_text(encoding="utf-8")

    lease, message = active_sessions.try_acquire_active_session(
        session_id="cli-cap-session", surface="cli", config={},
    )
    assert lease is not None and message is None
    assert "unknown-owner" in state_path.read_text(encoding="utf-8")


def test_unknown_sibling_liveness_only_fences_its_own_session(tmp_path, monkeypatch):
    """A desktop (liveness-tracked) claim, release and transfer survive an unrelated
    sibling whose owner pid exists but whose start time cannot be read (LXC /proc after
    a backend restart, #113683); the SAME session id still fails closed."""
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    active_sessions._write_entries(state_path, [{
        "lease_id": "stale-sibling", "session_id": "old-chat", "surface": "desktop",
        "pid": 1, "process_start_time": 1234.5, "track_liveness": True,
    }])
    original_start = active_sessions._process_start_time
    monkeypatch.setattr(
        active_sessions, "_process_start_time",
        lambda pid: None if int(pid) == 1 else original_start(pid),
    )
    assert active_sessions._pid_liveness(1, 1234.5) is None

    with pytest.raises(active_sessions.ActiveSessionRegistryError, match="liveness is unknown"):
        active_sessions.try_acquire_active_session(
            session_id="old-chat", surface="desktop", config={}, track_liveness=True,
        )

    lease, message = active_sessions.try_acquire_active_session(
        session_id="new-chat", surface="desktop", config={}, track_liveness=True,
    )
    assert lease is not None and message is None
    assert active_sessions.transfer_active_session(lease, session_id="new-chat-rotated")
    lease.release()
    assert lease.released is True
    remaining = active_sessions._read_entries(state_path)
    assert [e["lease_id"] for e in remaining] == ["stale-sibling"]


def test_unknown_sibling_does_not_block_guarded_release_or_orphan_sweep(tmp_path, monkeypatch):
    """Desktop automatic cleanup (orphan reap, disconnect, idle timeout) releases its
    lease through ``release_active_session_liveness_guard``; that path and the
    orphan sweep must tolerate an unknowable, unrelated sibling as well (#113683)."""
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    state_path = home / "runtime" / "active_sessions.json"
    active_sessions._write_entries(state_path, [{
        "lease_id": "stale-sibling", "session_id": "old-chat", "surface": "desktop",
        "pid": 1, "process_start_time": 1234.5, "track_liveness": True,
    }])
    original_start = active_sessions._process_start_time
    monkeypatch.setattr(
        active_sessions, "_process_start_time",
        lambda pid: None if int(pid) == 1 else original_start(pid),
    )
    lease, message = active_sessions.try_acquire_active_session(
        session_id="new-chat", surface="desktop", config={}, track_liveness=True,
    )
    assert lease is not None and message is None

    with active_sessions.release_active_session_liveness_guard(
        lease, "new-chat", own_live_lease_ids=set()
    ) as active:
        assert active is False
    assert lease.released is True
    with active_sessions.active_session_liveness_guard("new-chat") as active:
        assert active is False
    assert [e["lease_id"] for e in active_sessions._read_entries(state_path)] == ["stale-sibling"]

    orphan, _ = active_sessions.try_acquire_active_session(
        session_id="orphaned", surface="desktop", config={}, track_liveness=True,
    )
    assert orphan is not None
    _backdate_leases(home)
    assert active_sessions._release_orphaned_leases_in_home(home, set()) == 1
    assert [e["lease_id"] for e in active_sessions._read_entries(state_path)] == ["stale-sibling"]


def test_liveness_release_failure_is_retryable(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    lease, message = active_sessions.try_acquire_active_session(
        session_id="session-1",
        surface="desktop",
        config={},
        track_liveness=True,
    )
    assert lease is not None and message is None

    original_write = active_sessions._write_entries
    monkeypatch.setattr(
        active_sessions,
        "_write_entries",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("replace failed")),
    )
    with pytest.raises(OSError, match="replace failed"):
        lease.release()
    assert lease.released is False

    monkeypatch.setattr(active_sessions, "_write_entries", original_write)
    lease.release()
    assert lease.released is True
    assert active_sessions.active_session_registry_snapshot() == []


def test_liveness_transfer_upserts_missing_entry_without_consuming_a_new_slot(
    tmp_path, monkeypatch
):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    lease, message = active_sessions.try_acquire_active_session(
        session_id="session-old",
        surface="desktop",
        config={"max_concurrent_sessions": 1},
        track_liveness=True,
    )
    assert lease is not None and message is None
    (home / "runtime" / "active_sessions.json").unlink()

    assert active_sessions.transfer_active_session(lease, session_id="session-new")
    snapshot = active_sessions.active_session_registry_snapshot()
    assert [(entry["lease_id"], entry["session_id"]) for entry in snapshot] == [
        (lease.lease_id, "session-new")
    ]

    blocked, limit_message = active_sessions.try_acquire_active_session(
        session_id="session-other",
        surface="desktop",
        config={"max_concurrent_sessions": 1},
        track_liveness=True,
    )
    assert blocked is None
    assert limit_message is not None
    lease.release()


def test_liveness_transfer_write_failure_keeps_old_id_for_retry(tmp_path, monkeypatch):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    lease, message = active_sessions.try_acquire_active_session(
        session_id="session-old",
        surface="desktop",
        config={},
        track_liveness=True,
    )
    assert lease is not None and message is None

    original_write = active_sessions._write_entries
    monkeypatch.setattr(
        active_sessions,
        "_write_entries",
        lambda *_args, **_kwargs: (_ for _ in ()).throw(OSError("replace failed")),
    )
    with pytest.raises(OSError, match="replace failed"):
        active_sessions.transfer_active_session(lease, session_id="session-new")
    assert lease.session_id == "session-old"

    monkeypatch.setattr(active_sessions, "_write_entries", original_write)
    assert active_sessions.transfer_active_session(lease, session_id="session-new")
    assert lease.session_id == "session-new"
    lease.release()


def test_release_wins_against_transfer_waiting_on_same_lease_lock(
    tmp_path, monkeypatch
):
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    lease, message = active_sessions.try_acquire_active_session(
        session_id="session-old",
        surface="desktop",
        config={},
        track_liveness=True,
    )
    assert lease is not None and message is None

    release_wrote = threading.Event()
    allow_release = threading.Event()
    transfer_at_lock = threading.Event()
    original_write = active_sessions._write_entries
    original_enter = active_sessions._FileLock.__enter__

    def _blocking_write(path, entries):
        original_write(path, entries)
        if threading.current_thread().name == "lease-release":
            release_wrote.set()
            assert allow_release.wait(timeout=5)

    def _instrumented_enter(lock):
        if threading.current_thread().name == "lease-transfer":
            transfer_at_lock.set()
        return original_enter(lock)

    monkeypatch.setattr(active_sessions, "_write_entries", _blocking_write)
    monkeypatch.setattr(active_sessions._FileLock, "__enter__", _instrumented_enter)
    transfer_result: list[bool] = []
    release_thread = threading.Thread(target=lease.release, name="lease-release")
    transfer_thread = threading.Thread(
        target=lambda: transfer_result.append(
            active_sessions.transfer_active_session(lease, session_id="session-new")
        ),
        name="lease-transfer",
    )

    release_thread.start()
    assert release_wrote.wait(timeout=5)
    transfer_thread.start()
    assert transfer_at_lock.wait(timeout=5)
    allow_release.set()
    release_thread.join(timeout=5)
    transfer_thread.join(timeout=5)

    assert not release_thread.is_alive()
    assert not transfer_thread.is_alive()
    assert transfer_result == [False]
    assert lease.released is True
    assert active_sessions.active_session_registry_snapshot() == []



def test_liveness_guard_keeps_a_just_acquired_own_lease_it_cannot_vouch_for(
    tmp_path, monkeypatch
):
    """Race in #101415's fix: the finalizing session snapshots its live lease
    ids, then a sibling session acquires a lease before the registry lock is
    taken. That lease is absent from the snapshot but is not an orphan."""
    home = tmp_path / ".hermes"
    monkeypatch.setenv("HERMES_HOME", str(home))
    fresh, error = active_sessions.try_acquire_active_session(
        session_id="fresh", surface="desktop", config={}, registry_home=home
    )
    assert fresh is not None and error is None

    with active_sessions.active_session_liveness_guard(
        "fresh", registry_home=home, own_live_lease_ids=set()
    ) as active:
        assert active is True
    assert [e["lease_id"] for e in active_sessions.active_session_registry_snapshot(home)] == [fresh.lease_id]

    _backdate_leases(home)
    with active_sessions.active_session_liveness_guard(
        "fresh", registry_home=home, own_live_lease_ids=set()
    ) as active:
        assert active is False
    assert active_sessions.active_session_registry_snapshot(home) == []


def test_pid_liveness_self_pid_skips_exists_probe(monkeypatch):
    """The probing process is trivially live: no psutil sweep for os.getpid() (#108005)."""
    exists_calls: list = []

    def _count_exists(pid):
        exists_calls.append(pid)
        return True

    monkeypatch.setattr("gateway.status._pid_exists", _count_exists)
    assert active_sessions._pid_liveness(os.getpid()) is True
    # Identity is still (pid, start time): our pid with a start we never had is a recycled pid.
    assert active_sessions._pid_liveness(os.getpid(), 1.0) is False
    assert exists_calls == []


def test_snapshot_prunes_after_lock_release_and_keeps_concurrent_lease(tmp_path, monkeypatch):
    """#115578: ``active_session_registry_snapshot`` must not run the per-entry liveness
    probes while holding the exclusive registry lock (one psutil round-trip per lease held
    under an unfair ``LK_LOCK`` starves every 2 Hz poller past ~7 leases), and the pruned
    write-back must not drop a lease acquired between the snapshot and the write."""
    home = tmp_path / ".hermes"
    state_path = home / "runtime" / "active_sessions.json"
    lock_path = home / "runtime" / "active_sessions.lock"
    state_path.parent.mkdir(parents=True)
    dead_pid = 2**30  # never a live pid: _pid_exists() is False, nothing is signalled
    active_sessions._write_entries(state_path, [
        {"lease_id": "live-1", "session_id": "s-live", "pid": os.getpid()},
        {"lease_id": "dead-1", "session_id": "s-dead", "pid": dead_pid},
    ])
    newcomer = {"lease_id": "new-1", "session_id": "s-new", "pid": os.getpid()}
    real_prune = active_sessions._prune_dead
    acquired_during_prune = []

    def concurrent_acquire():
        with active_sessions._FileLock(lock_path):
            current = active_sessions._read_entries(state_path, strict=True)
            active_sessions._write_entries(state_path, current + [dict(newcomer)])

    def spy_prune(entries, **kwargs):
        # A concurrent acquirer must be able to take the lock while the probes run.
        acquirer = threading.Thread(target=concurrent_acquire, daemon=True)
        acquirer.start()
        acquirer.join(timeout=5)
        acquired_during_prune.append(not acquirer.is_alive())
        return real_prune(entries, **kwargs)

    monkeypatch.setattr(active_sessions, "_prune_dead", spy_prune)

    live = active_sessions.active_session_registry_snapshot(registry_home=home)

    assert acquired_during_prune == [True]
    assert [e["lease_id"] for e in live] == ["live-1"]
    persisted = active_sessions._read_entries(state_path, strict=True)
    assert sorted(e["lease_id"] for e in persisted) == ["live-1", "new-1"]
