"""
Tests for issue #2698: HERMES_HOME isolated profile mode.

When HERMES_HOME points at a specific profile directory like ~/.hermes/profiles/user1
AND isolated mode is explicitly opted into via HERMES_WEBUI_ISOLATED_PROFILE=1, the WebUI
should pin to that single profile: list only it, reject create/switch/delete of other
profiles, and hide multi-profile UI affordances.

Note (#4586): isolated mode now requires the explicit HERMES_WEBUI_ISOLATED_PROFILE opt-in
in addition to the profile-shaped HERMES_HOME — the shape alone is NOT sufficient, because a
normal single-user named profile produces the same shape. The autouse fixture below enables
the flag for this whole module (it tests the isolated-mode deployment posture); the
shape-without-flag regression is covered separately in test_issue4586_*.
"""

import os
import io
import sys
import tempfile
import types
from pathlib import Path
from unittest import mock

import pytest

import api.profiles as _profiles_mod
from api.profiles import (
    _is_isolated_profile_mode,
    clear_request_profile,
    list_profiles_api,
    create_profile_api,
    delete_profile_api,
    get_active_hermes_home,
    get_active_profile_name,
    init_profile_state,
    set_request_profile,
    switch_profile,
)


@pytest.fixture(autouse=True)
def _clear_profile_cache(monkeypatch):
    """Clear the profile list cache + enable the isolated-mode opt-in for every test.

    #4586: isolated mode is gated on the explicit HERMES_WEBUI_ISOLATED_PROFILE flag, so
    this whole module — which exercises isolated-mode behavior — enables it. Normal-mode
    assertions in this file still hold because they point HERMES_HOME at the base home, which
    fails the secondary shape requirement regardless of the flag.
    """
    monkeypatch.setenv("HERMES_WEBUI_ISOLATED_PROFILE", "1")
    monkeypatch.setattr(_profiles_mod, "_INITIAL_ISOLATED_PROFILE_OPT_IN", "1")
    _profiles_mod._LIST_PROFILES_CACHE = None
    yield
    _profiles_mod._LIST_PROFILES_CACHE = None


@pytest.fixture
def temp_hermes_home():
    """Create a temporary .hermes directory structure for testing."""
    with tempfile.TemporaryDirectory() as tmpdir:
        home = Path(tmpdir) / ".hermes"
        home.mkdir()
        profiles_root = home / "profiles"
        profiles_root.mkdir()
        yield home


@pytest.fixture
def temp_single_profile():
    """Create a temporary .hermes/profiles/user1 structure for isolated mode."""
    with tempfile.TemporaryDirectory() as tmpdir:
        home = Path(tmpdir) / ".hermes"
        home.mkdir()
        profiles_root = home / "profiles"
        profiles_root.mkdir()
        user1 = profiles_root / "user1"
        user1.mkdir()
        # Create required subdirs
        for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
            (user1 / subdir).mkdir(exist_ok=True)
        yield user1


class TestIsolatedProfileModeDetection:
    """Test _is_isolated_profile_mode() helper."""

    def test_normal_mode_when_hermes_home_is_base(self, temp_hermes_home):
        """Normal mode when HERMES_HOME points to base ~/.hermes."""
        with mock.patch.dict(os.environ, {"HERMES_HOME": str(temp_hermes_home)}):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", temp_hermes_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_hermes_home)):
                    isolated = _is_isolated_profile_mode()
                    assert isolated is False

    def test_isolated_mode_when_hermes_home_is_profile_subdir(self, temp_single_profile):
        """Isolated mode when HERMES_HOME points to ~/.hermes/profiles/user1."""
        # Ensure we're in the fixture context where temp_single_profile exists
        assert temp_single_profile.exists(), f"Test fixture path doesn't exist: {temp_single_profile}"
        assert temp_single_profile.parent.name == "profiles", f"Parent not named 'profiles': {temp_single_profile.parent}"

        # _is_isolated_profile_mode() uses _INITIAL_HERMES_HOME (snapshotted at import time),
        # not the current os.environ value. Patch both for this test.
        with mock.patch.dict(os.environ, {"HERMES_HOME": str(temp_single_profile)}, clear=False):
            with mock.patch.dict(os.environ, {"HERMES_BASE_HOME": ""}, clear=False):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    isolated = _is_isolated_profile_mode()
                    assert isolated is True, f"Expected isolated mode for {temp_single_profile}"

    def test_hermes_base_home_does_not_disable_isolation(self, temp_single_profile):
        """HERMES_BASE_HOME must not disable isolation for a profiles/<name> path."""
        base_home = temp_single_profile.parent.parent
        with mock.patch.dict(
            os.environ,
            {
                "HERMES_HOME": str(temp_single_profile),
                "HERMES_BASE_HOME": str(base_home),
            },
        ):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    isolated = _is_isolated_profile_mode()
                    assert isolated is True


class TestListProfilesInIsolatedMode:
    """Test list_profiles_api() returns only isolated profile when in isolated mode."""

    def test_list_returns_all_profiles_in_normal_mode(self, temp_hermes_home):
        """Normal mode lists all profiles."""
        # Create a few test profiles
        profiles_root = temp_hermes_home / "profiles"
        (profiles_root / "user1").mkdir()
        (profiles_root / "user2").mkdir()
        (profiles_root / "user3").mkdir()

        # Create required subdirs for each
        for prof_dir in profiles_root.iterdir():
            if prof_dir.is_dir():
                for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
                    (prof_dir / subdir).mkdir(exist_ok=True)

        with mock.patch.dict(os.environ, {"HERMES_HOME": str(temp_hermes_home)}):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", temp_hermes_home):
                with mock.patch("api.profiles._is_isolated_profile_mode", return_value=False):
                    # Mock _get_profile_skills_stats to avoid importing agent.skill_utils
                    with mock.patch("api.profiles._get_profile_skills_stats", return_value=(0, 0)):
                        # Mock _build_profile_rows_fast to return the expected profiles
                        def mock_build_profiles():
                            return [
                                {'name': 'default', 'path': str(temp_hermes_home), 'is_default': True,
                                 'is_active': False, 'gateway_running': False, 'model': None, 'provider': None,
                                 'has_env': False, 'visible': True, 'skill_count': 0, 'enabled_skills': 0, 'total_skills': 0},
                                {'name': 'user1', 'path': str(profiles_root / 'user1'), 'is_default': False,
                                 'is_active': False, 'gateway_running': False, 'model': None, 'provider': None,
                                 'has_env': False, 'visible': True, 'skill_count': 0, 'enabled_skills': 0, 'total_skills': 0},
                                {'name': 'user2', 'path': str(profiles_root / 'user2'), 'is_default': False,
                                 'is_active': False, 'gateway_running': False, 'model': None, 'provider': None,
                                 'has_env': False, 'visible': True, 'skill_count': 0, 'enabled_skills': 0, 'total_skills': 0},
                                {'name': 'user3', 'path': str(profiles_root / 'user3'), 'is_default': False,
                                 'is_active': False, 'gateway_running': False, 'model': None, 'provider': None,
                                 'has_env': False, 'visible': True, 'skill_count': 0, 'enabled_skills': 0, 'total_skills': 0},
                            ]
                        with mock.patch("api.profiles._build_profile_rows_fast", side_effect=mock_build_profiles):
                            profiles = list_profiles_api()
                            # Should have at least 'default' plus the created profiles
                            names = [p["name"] for p in profiles]
                            assert "user1" in names
                            assert "user2" in names
                            assert "user3" in names

    def test_list_returns_only_isolated_profile_in_isolated_mode(self, temp_single_profile):
        """Isolated mode lists only the configured profile."""
        base_home = temp_single_profile.parent.parent
        # Create other profiles that should be hidden
        other_profiles = base_home / "profiles"
        (other_profiles / "user2").mkdir()
        (other_profiles / "user3").mkdir()
        for prof_dir in [other_profiles / "user2", other_profiles / "user3"]:
            for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
                (prof_dir / subdir).mkdir(exist_ok=True)

        # Clear HERMES_BASE_HOME to allow isolated mode detection to fire
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    # Mock _get_profile_skills_stats to avoid importing agent.skill_utils
                    with mock.patch("api.profiles._get_profile_skills_stats", return_value=(0, 0)):
                        profiles = list_profiles_api()
                        # Should only have user1
                        assert len(profiles) == 1
                        assert profiles[0]["name"] == "user1"

    def test_list_includes_single_profile_mode_flag(self, temp_single_profile):
        """Response includes single_profile_mode: true in isolated mode."""
        base_home = temp_single_profile.parent.parent
        with mock.patch.dict(os.environ, {"HERMES_HOME": str(temp_single_profile)}):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    with mock.patch("api.profiles._is_isolated_profile_mode", return_value=True):
                        with mock.patch("api.profiles._resolve_base_hermes_home", return_value=base_home):
                            # Mock _get_profile_skills_stats to avoid importing agent.skill_utils
                            with mock.patch("api.profiles._get_profile_skills_stats", return_value=(0, 0)):
                                profiles = list_profiles_api()
                                # Check for single_profile_mode flag in response structure
                                # For now, profiles should be a list; the flag will be in routes.py response
                                assert len(profiles) == 1


class TestIsolatedRuntimePinning:
    """Test that isolated mode pins both detection and active runtime home."""

    def test_get_active_profile_name_ignores_tls_and_global_in_isolated_mode(self, temp_single_profile):
        """Isolated mode must ignore request TLS and process-global active profile."""
        base_home = temp_single_profile.parent.parent
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    with mock.patch("api.profiles._active_profile", "other_profile"):
                        set_request_profile("tls_profile")
                        try:
                            assert get_active_profile_name() == "user1"
                        finally:
                            clear_request_profile()

    @pytest.mark.parametrize("active_profile_contents", [None, "user2"])
    def test_init_profile_state_pins_runtime_home_to_isolated_profile(
        self,
        temp_single_profile,
        monkeypatch,
        active_profile_contents,
    ):
        """init_profile_state must ignore the base active_profile file in isolated mode."""
        base_home = temp_single_profile.parent.parent
        other_profile = base_home / "profiles" / "user2"
        other_profile.mkdir()
        for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
            (other_profile / subdir).mkdir(exist_ok=True)

        active_profile_file = base_home / "active_profile"
        if active_profile_contents is None:
            active_profile_file.unlink(missing_ok=True)
        else:
            active_profile_file.write_text(active_profile_contents, encoding="utf-8")

        monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))
        monkeypatch.setattr(_profiles_mod, "_active_profile", "default")
        monkeypatch.setattr(_profiles_mod, "install_cron_scheduler_profile_isolation", lambda: None)

        reloaded = []
        real_reload_dotenv = _profiles_mod._reload_dotenv

        def _record_reload(home):
            reloaded.append(home)
            return real_reload_dotenv(home)

        monkeypatch.setattr(_profiles_mod, "_reload_dotenv", _record_reload)

        init_profile_state()

        assert _profiles_mod._active_profile == "user1"
        assert get_active_profile_name() == "user1"
        assert get_active_hermes_home() == temp_single_profile
        assert Path(os.environ["HERMES_HOME"]) == temp_single_profile
        assert reloaded == [temp_single_profile]

    def test_get_active_hermes_home_keeps_profiles_default_pinned(self, temp_hermes_home, monkeypatch):
        """An isolated profiles/default path must not collapse back to the base home."""
        isolated_default = temp_hermes_home / "profiles" / "default"
        isolated_default.mkdir()
        for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
            (isolated_default / subdir).mkdir(exist_ok=True)

        monkeypatch.setenv("HERMES_HOME", str(isolated_default))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", temp_hermes_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(isolated_default))
        monkeypatch.setattr(_profiles_mod, "_active_profile", "other_profile")

        assert get_active_profile_name() == "default"
        assert get_active_hermes_home() == isolated_default

    def test_explicit_profile_resolution_for_isolated_default_uses_pinned_home(self, temp_hermes_home, monkeypatch):
        """Explicit default-profile resolution must stay on the isolated home."""
        from api.profiles import get_hermes_home_for_profile, _resolve_profile_home_for_name

        isolated_default = temp_hermes_home / "profiles" / "default"
        isolated_default.mkdir()
        for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
            (isolated_default / subdir).mkdir(exist_ok=True)

        monkeypatch.setenv("HERMES_HOME", str(isolated_default))
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", temp_hermes_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(isolated_default))

        assert _resolve_profile_home_for_name("default") == isolated_default
        assert get_hermes_home_for_profile("default") == isolated_default
        assert get_hermes_home_for_profile("default") != temp_hermes_home

    def test_explicit_foreign_profile_resolution_stays_on_isolated_home(self, temp_single_profile, monkeypatch):
        """Explicit profile lookups must not escape the isolated startup home."""
        from api.profiles import get_hermes_home_for_profile, _resolve_profile_home_for_name

        base_home = temp_single_profile.parent.parent
        monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))

        assert _resolve_profile_home_for_name("other_profile") == temp_single_profile
        assert get_hermes_home_for_profile("other_profile") == temp_single_profile

    def test_list_profiles_prefers_matching_isolated_default_home(self, temp_hermes_home, monkeypatch):
        """The isolated profiles/default row must not collapse to the base-home row."""
        isolated_default = temp_hermes_home / "profiles" / "default"
        isolated_default.mkdir(parents=True)
        for subdir in ["memories", "sessions", "skills", "skins", "logs", "plans", "workspace", "cron"]:
            (isolated_default / subdir).mkdir(exist_ok=True)

        class _Info:
            def __init__(self, name, path, is_default):
                self.name = name
                self.path = path
                self.is_default = is_default
                self.gateway_running = False
                self.model = None
                self.provider = None
                self.has_env = False

        monkeypatch.setenv("HERMES_HOME", str(isolated_default))
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", temp_hermes_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(isolated_default))
        monkeypatch.setattr(_profiles_mod, "_get_profile_skills_stats", lambda _path: (0, 0))

        # Inject a STUB hermes_cli.profiles into sys.modules rather than
        # mock.patch("hermes_cli.profiles.list_profiles", ...): that string
        # target forces a real import of hermes_cli, which is NOT installed in
        # CI (the bundled agent isn't on the WebUI test path) -> ModuleNotFoundError.
        # list_profiles_api() does `from hermes_cli.profiles import list_profiles`
        # internally, so a stub module satisfies it at the WebUI boundary. (#4454 CI)
        stub_profiles = types.ModuleType("hermes_cli.profiles")
        stub_profiles.list_profiles = lambda: [
            _Info("default", temp_hermes_home, True),
            _Info("default", isolated_default, False),
        ]
        stub_pkg = sys.modules.get("hermes_cli") or types.ModuleType("hermes_cli")
        monkeypatch.setitem(sys.modules, "hermes_cli", stub_pkg)
        monkeypatch.setitem(sys.modules, "hermes_cli.profiles", stub_profiles)

        rows = list_profiles_api()

        assert len(rows) == 1
        assert Path(rows[0]["path"]) == isolated_default

    def test_list_profiles_isolated_fallback_does_not_reenter_root_profile_lookup(self, temp_single_profile, monkeypatch):
        """The isolated fallback must not recurse back through _is_root_profile."""
        base_home = temp_single_profile.parent.parent
        monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))
        monkeypatch.setattr(_profiles_mod, "_get_profile_skills_stats", lambda _path: (0, 0))
        monkeypatch.setattr(
            _profiles_mod,
            "_is_root_profile",
            lambda _name: (_ for _ in ()).throw(AssertionError("_is_root_profile should not run in isolated fallback")),
        )

        with mock.patch.dict(sys.modules, {"hermes_cli": None, "hermes_cli.profiles": None}):
            rows = list_profiles_api()

        assert len(rows) == 1
        assert rows[0]["name"] == "user1"
        assert rows[0]["is_default"] is False


class TestProfileMutationsInIsolatedMode:
    """Test that create/delete/switch are rejected (403) in isolated mode."""

    def test_create_profile_rejected_in_isolated_mode(self, temp_single_profile):
        """create_profile_api should reject creation in isolated mode."""
        base_home = temp_single_profile.parent.parent
        # Clear HERMES_BASE_HOME to allow isolated mode detection to fire
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    with pytest.raises(PermissionError, match=".*isolated.*|.*single.*"):
                        create_profile_api("newprofile")

    def test_delete_profile_rejected_in_isolated_mode(self, temp_single_profile):
        """delete_profile_api should reject deletion in isolated mode."""
        base_home = temp_single_profile.parent.parent
        # Clear HERMES_BASE_HOME to allow isolated mode detection to fire
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    with pytest.raises(PermissionError, match=".*isolated.*|.*single.*"):
                        delete_profile_api("user1")

    def test_switch_to_different_profile_rejected(self, temp_single_profile):
        """switch_profile should reject switching to another profile in isolated mode."""
        base_home = temp_single_profile.parent.parent
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    with pytest.raises(PermissionError, match=".*isolated.*|.*pinned.*"):
                        switch_profile("other_user")

    def test_switch_to_same_profile_idempotent(self, temp_single_profile):
        """switch_profile to the isolated profile itself should pass through."""
        base_home = temp_single_profile.parent.parent
        env_dict = {
            "HERMES_HOME": str(temp_single_profile),
            "HERMES_BASE_HOME": "",
        }
        with mock.patch.dict(os.environ, env_dict, clear=False):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", base_home):
                with mock.patch("api.profiles._INITIAL_HERMES_HOME", str(temp_single_profile)):
                    # Should not raise PermissionError; may fail downstream
                    # for other reasons (missing hermes_cli), but the isolation
                    # guard must pass for the same-name case.
                    try:
                        switch_profile("user1")
                    except PermissionError:
                        pytest.fail("switch_profile should allow switching to the isolated profile itself")
                    except (ImportError, ValueError, RuntimeError):
                        pass  # expected in test env without hermes_cli

    def test_switch_to_same_default_profile_keeps_pinned_home(self, temp_hermes_home, monkeypatch, tmp_path):
        """A same-name switch for isolated profiles/default must keep using the pinned home."""
        isolated_default = temp_hermes_home / "profiles" / "default"
        isolated_default.mkdir(parents=True)
        (isolated_default / "workspace").mkdir()
        base_workspace = tmp_path / "base-workspace"
        isolated_workspace = tmp_path / "isolated-workspace"
        base_workspace.mkdir()
        isolated_workspace.mkdir()
        (temp_hermes_home / "config.yaml").write_text(f"workspace: {base_workspace}\n", encoding="utf-8")
        (isolated_default / "config.yaml").write_text(f"workspace: {isolated_workspace}\n", encoding="utf-8")

        monkeypatch.setenv("HERMES_HOME", str(isolated_default))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", temp_hermes_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(isolated_default))
        monkeypatch.setattr(_profiles_mod, "list_profiles_api", lambda: [])

        result = switch_profile("default", process_wide=False)

        assert result["default_workspace"] == str(isolated_workspace.resolve())

    def test_scheduled_cron_jobs_stay_pinned_to_isolated_home(self, temp_single_profile, monkeypatch):
        """Scheduler jobs must not resolve foreign profile homes in isolated mode."""
        base_home = temp_single_profile.parent.parent
        monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))

        assert _profiles_mod._home_for_scheduled_cron_job({"id": "job2698", "profile": "other"}) == temp_single_profile

    def test_cli_import_all_profiles_is_rejected_in_isolated_mode(self, monkeypatch):
        """Direct all_profiles CLI imports must not bypass isolated-profile boundaries."""
        import api.routes as routes

        captured = {}

        class _Handler:
            def __init__(self):
                self.wfile = io.BytesIO()

            def send_response(self, status):
                self.status = status

            def send_header(self, key, value):
                pass

            def end_headers(self):
                pass

        monkeypatch.setattr(routes, "_is_isolated_profile_mode", lambda: True)
        monkeypatch.setattr(
            routes,
            "bad",
            lambda h, m, c=400: (captured.__setitem__("bad", (m, c)), True)[1],
        )

        routes._handle_session_import_cli(
            _Handler(),
            {"session_id": "foreign-cli-2698", "all_profiles": 1, "profile": "other"},
        )

        assert captured["bad"] == (
            "all_profiles import is not allowed in isolated profile mode",
            403,
        )

    def test_scheduler_publishes_isolated_profile_after_foreign_job_profile(self, temp_single_profile, monkeypatch):
        """Scheduled cron completion must publish the isolated profile identity."""
        events = []
        base_home = temp_single_profile.parent.parent

        cron_pkg = types.ModuleType("cron")
        cron_pkg.__path__ = []
        cron_scheduler = types.ModuleType("cron.scheduler")
        cron_scheduler.run_job = lambda job: events.append(("run", job["id"])) or "ok"

        class _Ctx:
            def __init__(self, home):
                self.home = str(home)

            def __enter__(self):
                events.append(("enter", self.home))
                return self

            def __exit__(self, exc_type, exc, tb):
                events.append(("exit", self.home))
                return False

        monkeypatch.setitem(sys.modules, "cron", cron_pkg)
        monkeypatch.setitem(sys.modules, "cron.scheduler", cron_scheduler)
        monkeypatch.setenv("HERMES_HOME", str(temp_single_profile))
        monkeypatch.setenv("HERMES_BASE_HOME", "")
        monkeypatch.setattr(_profiles_mod, "_DEFAULT_HERMES_HOME", base_home)
        monkeypatch.setattr(_profiles_mod, "_INITIAL_HERMES_HOME", str(temp_single_profile))
        monkeypatch.setattr(_profiles_mod, "cron_profile_context_for_home", _Ctx)
        monkeypatch.setattr(
            _profiles_mod,
            "publish_session_list_changed",
            lambda reason, profile=None: events.append(("publish", reason, profile)),
        )

        _profiles_mod.install_cron_scheduler_profile_isolation()

        assert cron_scheduler.run_job({"id": "job2698", "profile": "other"}) == "ok"
        assert events[-1] == ("publish", "cron_complete", "user1")


class TestNormalModePreservation:
    """Test that normal mode behavior is completely unchanged."""

    def test_normal_mode_profile_operations_work(self, temp_hermes_home):
        """Normal mode allows profile creation and deletion."""
        with mock.patch.dict(os.environ, {"HERMES_HOME": str(temp_hermes_home)}):
            with mock.patch("api.profiles._DEFAULT_HERMES_HOME", temp_hermes_home):
                with mock.patch("api.profiles._is_isolated_profile_mode", return_value=False):
                    # Normal mode should not raise errors for create/delete operations
                    # (though they may fail for other reasons in this test environment)
                    try:
                        # Just verify the isolation guard doesn't trigger
                        from api.profiles import create_profile_api
                        # The actual call might fail due to missing hermes_cli,
                        # but should NOT fail with an "isolated mode" error
                        try:
                            create_profile_api("testprof1")
                        except ValueError as e:
                            # Should be a different error, not about isolation
                            assert "isolated" not in str(e).lower()
                            assert "single" not in str(e).lower()
                    except ImportError:
                        # hermes_cli not available, skip
                        pass


def test_profiles_panel_hides_delete_controls_in_single_profile_mode():
    panels_js = (Path(__file__).resolve().parents[1] / "static" / "panels.js").read_text(encoding="utf-8")

    assert "const singleProfileMode = !!(_profilesCache && _profilesCache.single_profile_mode);" in panels_js
    assert "if (isDefault || singleProfileMode) hide(delBtn); else show(delBtn);" in panels_js
