"""
Regression tests for the shared-container task_id mapping.

The top-level agent and all delegate_task subagents share a single
terminal sandbox keyed by ``"default"``.  ``_resolve_container_task_id``
is the sole gatekeeper for which tool-call task_ids go to the shared
container vs. get their own isolated sandbox.  RL / benchmark
environments opt in to isolation by calling
``register_task_env_overrides(task_id, {...})`` before the agent loop;
every other task_id collapses back to ``"default"``.

If you change the collapse logic, update both the helper and these
tests -- see `hermes-agent-dev` skill, "Why do subagents get their own
containers?" section, and the Container lifecycle paragraph under
Docker Backend in ``website/docs/user-guide/configuration.md``.
"""

import pytest

from tools import terminal_tool


@pytest.fixture(autouse=True)
def _clean_overrides():
    """Ensure no stray overrides from other tests leak in."""
    before = dict(terminal_tool._task_env_overrides)
    terminal_tool._task_env_overrides.clear()
    yield
    terminal_tool._task_env_overrides.clear()
    terminal_tool._task_env_overrides.update(before)


def test_none_task_id_maps_to_default():
    assert terminal_tool._resolve_container_task_id(None) == "default"


def test_cwd_only_override_collapses_to_default():
    """CWD-only overrides (ACP adapter workspace tracking) must NOT trigger
    container isolation — they should collapse to the shared 'default'
    container so all surfaces (TUI, gateway, dashboard) share one sandbox.
    Regression for #37361."""
    terminal_tool.register_task_env_overrides(
        "acp-session-abc", {"cwd": "/home/user/project"}
    )
    try:
        assert (
            terminal_tool._resolve_container_task_id("acp-session-abc")
            == "default"
        )
    finally:
        terminal_tool.clear_task_env_overrides("acp-session-abc")


def test_env_type_override_keeps_own_id():
    """env_type is an isolation key — must trigger per-task container."""
    terminal_tool.register_task_env_overrides(
        "bench-env", {"env_type": "sandbox", "cwd": "/work"}
    )
    try:
        assert (
            terminal_tool._resolve_container_task_id("bench-env")
            == "bench-env"
        )
    finally:
        terminal_tool.clear_task_env_overrides("bench-env")


# --- Cross-profile SSH-leak isolation (commit e00f940a9, re-applied) ---------
#
# When a session key is present (WebUI/gateway), each session must own its own
# slot in _active_environments so switching from profile A (ssh_host=10.0.0.1)
# to profile B (ssh_host=10.0.0.2) cannot reuse A's SSHEnvironment. Without this
# the shared "default" slot silently runs commands on the wrong remote host.


def test_distinct_session_keys_get_distinct_slots(monkeypatch):
    monkeypatch.setenv("HERMES_SESSION_KEY", "sess-A")
    a = terminal_tool._resolve_container_task_id(None)
    monkeypatch.setenv("HERMES_SESSION_KEY", "sess-B")
    b = terminal_tool._resolve_container_task_id(None)
    assert a == "session:sess-A"
    assert b == "session:sess-B"
    assert a != b


def test_subagent_collapses_onto_parent_session(monkeypatch):
    # Subagents inherit the parent's session key, so they share the parent's
    # container (the #16177 intent) rather than a global "default".
    monkeypatch.setenv("HERMES_SESSION_KEY", "sess-A")
    assert (
        terminal_tool._resolve_container_task_id("subagent-3-cafef00d")
        == "session:sess-A"
    )


def test_rl_override_wins_over_session_key(monkeypatch):
    monkeypatch.setenv("HERMES_SESSION_KEY", "sess-A")
    terminal_tool.register_task_env_overrides("tb2-z", {"docker_image": "z:1"})
    try:
        assert terminal_tool._resolve_container_task_id("tb2-z") == "tb2-z"
    finally:
        terminal_tool.clear_task_env_overrides("tb2-z")


def test_no_session_key_still_defaults(monkeypatch):
    # CLI mode: no session key -> unchanged "default" behaviour.
    monkeypatch.delenv("HERMES_SESSION_KEY", raising=False)
    assert terminal_tool._resolve_container_task_id(None) == "default"


# --- Production gateway path: session key bound via ContextVars ---------------
#
# The tests above set HERMES_SESSION_KEY through os.environ, which only
# exercises the os.getenv() *fallback* branch of the scoping logic. Real
# gateway turns never write this process-global env var — they bind the
# identity through gateway.session_context.set_session_vars(), which stores it
# in a ContextVar, and _resolve_container_task_id reads it back via
# get_session_env(). These companion tests cover that production path with
# HERMES_SESSION_KEY absent from os.environ.


def test_session_key_from_contextvar_without_environ(monkeypatch):
    # Prove the fix works on the gateway path: HERMES_SESSION_KEY is NOT in
    # os.environ; the key lives only in the ContextVar bound by the gateway.
    from gateway.session_context import clear_session_vars, set_session_vars

    monkeypatch.delenv("HERMES_SESSION_KEY", raising=False)
    tokens = set_session_vars(session_key="sess-ctx")
    try:
        assert (
            terminal_tool._resolve_container_task_id(None) == "session:sess-ctx"
        )
        # Subagents inherit the same ContextVar and collapse onto the parent.
        assert (
            terminal_tool._resolve_container_task_id("subagent-1-cafe")
            == "session:sess-ctx"
        )
    finally:
        clear_session_vars(tokens)


def test_contextvar_session_key_wins_over_environ(monkeypatch):
    # Two concurrent gateway sessions in one process must not cross-contaminate:
    # the ContextVar is authoritative even when a *different* value lingers in
    # os.environ (e.g. a CLI-set or previously-leaked global). The container
    # slot must follow the ContextVar-bound session, not the process global.
    from gateway.session_context import clear_session_vars, set_session_vars

    monkeypatch.setenv("HERMES_SESSION_KEY", "sess-ENV")
    tokens = set_session_vars(session_key="sess-CTX")
    try:
        assert (
            terminal_tool._resolve_container_task_id(None) == "session:sess-CTX"
        )
    finally:
        clear_session_vars(tokens)


# --- Persistent Docker is PROFILE-scoped, not session-scoped ------------------
#
# Product contract: TERMINAL_ENV=docker + container_persistent:true means ONE
# long-lived container per Hermes profile, shared by every session of that
# profile (CLI, gateway chats, WebUI). The a270c4ade session-key fallback must
# NOT fragment persistent Docker into per-session containers; it exists for
# backends where cross-session reuse is dangerous (SSH).


def _persistent_docker(monkeypatch):
    monkeypatch.setenv("TERMINAL_ENV", "docker")
    monkeypatch.setenv("TERMINAL_CONTAINER_PERSISTENT", "true")


def test_persistent_docker_default_profile_shares_default_container(monkeypatch):
    # A gateway session of the default profile lands on the SAME container key
    # CLI mode uses — "default" — not a per-session key.
    from gateway.session_context import clear_session_vars, set_session_vars

    _persistent_docker(monkeypatch)
    tokens = set_session_vars(session_key="agent:main:telegram:dm:123", profile="")
    try:
        assert terminal_tool._resolve_container_task_id(None) == "default"
    finally:
        clear_session_vars(tokens)


def test_persistent_docker_two_sessions_same_profile_share_container(monkeypatch):
    from gateway.session_context import clear_session_vars, set_session_vars

    _persistent_docker(monkeypatch)
    tokens = set_session_vars(session_key="agent:main:telegram:dm:123", profile="work")
    try:
        a = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    tokens = set_session_vars(session_key="agent:main:discord:guild:456", profile="work")
    try:
        b = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    assert a == b == "profile:work"


def test_persistent_docker_distinct_profiles_get_distinct_containers(monkeypatch):
    from gateway.session_context import clear_session_vars, set_session_vars

    _persistent_docker(monkeypatch)
    tokens = set_session_vars(session_key="sess-X", profile="work")
    try:
        a = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    tokens = set_session_vars(session_key="sess-X", profile="research")
    try:
        b = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    assert a == "profile:work"
    assert b == "profile:research"
    assert a != b


def test_nonpersistent_docker_keeps_session_scoping(monkeypatch):
    # container_persistent:false is an explicit isolation statement (#82731) —
    # the profile-scope gate must not fire there.
    from gateway.session_context import clear_session_vars, set_session_vars

    monkeypatch.setenv("TERMINAL_ENV", "docker")
    monkeypatch.setenv("TERMINAL_CONTAINER_PERSISTENT", "false")
    tokens = set_session_vars(session_key="sess-A", profile="work")
    try:
        assert terminal_tool._resolve_container_task_id(None) == "session:sess-A"
    finally:
        clear_session_vars(tokens)


def test_ssh_backend_keeps_session_scoping(monkeypatch):
    # The original a270c4ade leak: SSH environments must stay session-scoped
    # regardless of profile, or profile A's SSHEnvironment leaks into B.
    from gateway.session_context import clear_session_vars, set_session_vars

    monkeypatch.setenv("TERMINAL_ENV", "ssh")
    tokens = set_session_vars(session_key="sess-A", profile="work")
    try:
        assert terminal_tool._resolve_container_task_id(None) == "session:sess-A"
    finally:
        clear_session_vars(tokens)


# --- Trusted-profiles shared container opt-in (#84671) ------------------------


def test_shared_key_unifies_profiles(monkeypatch):
    from gateway.session_context import clear_session_vars, set_session_vars

    _persistent_docker(monkeypatch)
    monkeypatch.setenv("TERMINAL_DOCKER_SHARED_CONTAINER_KEY", "team/workspace")
    tokens = set_session_vars(session_key="s1", profile="work")
    try:
        a = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    tokens = set_session_vars(session_key="s2", profile="research")
    try:
        b = terminal_tool._resolve_container_task_id(None)
    finally:
        clear_session_vars(tokens)
    assert a == b == "shared:team/workspace"


def test_shared_key_applies_to_cli_no_session(monkeypatch):
    # CLI (no session key) must land in the same shared container as gateway
    # sessions, or the opt-in splits the container it exists to unify.
    _persistent_docker(monkeypatch)
    monkeypatch.setenv("TERMINAL_DOCKER_SHARED_CONTAINER_KEY", "team/workspace")
    monkeypatch.delenv("HERMES_SESSION_KEY", raising=False)
    assert terminal_tool._resolve_container_task_id(None) == "shared:team/workspace"


def test_empty_shared_key_keeps_profile_scoping(monkeypatch):
    from gateway.session_context import clear_session_vars, set_session_vars

    _persistent_docker(monkeypatch)
    monkeypatch.setenv("TERMINAL_DOCKER_SHARED_CONTAINER_KEY", "")
    tokens = set_session_vars(session_key="s1", profile="work")
    try:
        assert terminal_tool._resolve_container_task_id(None) == "profile:work"
    finally:
        clear_session_vars(tokens)


def test_shared_key_ignored_outside_persistent_docker(monkeypatch):
    # The opt-in is a persistent-Docker concept only: SSH keeps session
    # scoping even when the key is set.
    from gateway.session_context import clear_session_vars, set_session_vars

    monkeypatch.setenv("TERMINAL_ENV", "ssh")
    monkeypatch.setenv("TERMINAL_DOCKER_SHARED_CONTAINER_KEY", "team/workspace")
    tokens = set_session_vars(session_key="sess-A", profile="work")
    try:
        assert terminal_tool._resolve_container_task_id(None) == "session:sess-A"
    finally:
        clear_session_vars(tokens)
