"""Regression coverage for #3294 — profile tool configuration not respected in WebUI.

When a non-default profile leaves the per-session "Tool Restrictions"
(`enabled_toolsets`) blank, the streaming worker must resolve toolsets (and the
rest of the per-profile config: prefill context, fallback chains) from the
*session's own profile* config.yaml — not from the process-global ``default``
profile.

Root cause: the streaming agent runs on a detached ``threading.Thread`` that
does NOT inherit the per-request thread-local profile context (set from the
``hermes_profile`` cookie on the HTTP handler thread). On that worker the
ambient ``get_config()`` resolves through ``get_active_profile_name()`` which
falls back to the process-global ``_active_profile`` (usually ``default``). A
profile B with an empty ``platform_toolsets.cli`` would therefore load the
default profile's full toolset list, inflating the prompt from ~400 to ~15K
tokens (the reporter's symptom).

Fix: ``api.config.get_config_for_profile_home(profile_home)`` reads the config
for an explicit profile home directly off disk, bypassing the thread-local
resolver. The streaming worker passes the session's own resolved profile home.
"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

REPO = Path(__file__).resolve().parent.parent
if str(REPO) not in sys.path:
    sys.path.insert(0, str(REPO))


def _write_cfg(home: Path, cli_toolsets) -> None:
    home.mkdir(parents=True, exist_ok=True)
    import yaml

    home.joinpath("config.yaml").write_text(
        yaml.safe_dump({"platform_toolsets": {"cli": cli_toolsets}}, sort_keys=False),
        encoding="utf-8",
    )


@pytest.fixture
def _two_profiles(tmp_path, monkeypatch):
    """default profile = all tools; profile B = empty toolset list."""
    from api import config as cfg

    default_home = tmp_path / "default"
    profile_b_home = tmp_path / "profiles" / "b"
    _write_cfg(default_home, ["terminal", "file", "web", "search", "browser"])
    _write_cfg(profile_b_home, [])

    # Point the ambient resolver at the DEFAULT profile, simulating a worker
    # thread that fell back to the process-global default profile.
    monkeypatch.setenv("HERMES_CONFIG_PATH", str(default_home / "config.yaml"))
    cfg.reload_config()
    yield cfg, default_home, profile_b_home


def test_ambient_get_config_reads_default_profile(_two_profiles):
    """Baseline: the ambient resolver sees the DEFAULT profile (the bug source)."""
    cfg, default_home, profile_b_home = _two_profiles
    ambient = cfg.get_config()
    assert ambient.get("platform_toolsets", {}).get("cli") == [
        "terminal",
        "file",
        "web",
        "search",
        "browser",
    ]


def test_get_config_for_profile_home_reads_session_profile(_two_profiles):
    """The fix: explicitly resolving profile B's home reads B's empty toolsets,
    even though the ambient resolver still points at the default profile."""
    cfg, default_home, profile_b_home = _two_profiles

    profile_b_cfg = cfg.get_config_for_profile_home(profile_b_home)
    assert profile_b_cfg.get("platform_toolsets", {}).get("cli") == []

    # And the toolset resolver run against profile B's config must yield a
    # strictly smaller set than the default profile's — the token-inflation
    # the reporter observed comes from resolving against the wrong (default)
    # profile's full list.
    default_toolsets = set(cfg._resolve_cli_toolsets(cfg.get_config()))
    profile_b_toolsets = set(cfg._resolve_cli_toolsets(profile_b_cfg))
    assert profile_b_toolsets < default_toolsets
    assert "terminal" in default_toolsets
    assert "terminal" not in profile_b_toolsets
    assert "browser" not in profile_b_toolsets


def test_matching_home_defers_to_get_config(_two_profiles):
    """When the requested home matches the ambient path, defer to get_config()
    so in-memory overrides (monkeypatched cfg / runtime overrides) are honored."""
    cfg, default_home, profile_b_home = _two_profiles
    same = cfg.get_config_for_profile_home(default_home)
    # Same dict identity path as get_config() (cached) — verify it returns the
    # default profile's config, not a fresh disk read that could miss overrides.
    assert same.get("platform_toolsets", {}).get("cli") == [
        "terminal",
        "file",
        "web",
        "search",
        "browser",
    ]


def test_active_home_with_external_config_override_defers_to_get_config(_two_profiles, monkeypatch, tmp_path):
    """The active home still honors HERMES_CONFIG_PATH when the override lives
    outside that home."""
    cfg, default_home, profile_b_home = _two_profiles
    from api import profiles

    active_home = tmp_path / "active-home"
    override_path = tmp_path / "override-dir" / "config.yaml"
    active_home.mkdir()
    _write_cfg(active_home, ["wrong-active-home-file"])
    override_path.parent.mkdir()
    import yaml

    override_path.write_text(
        yaml.safe_dump({"platform_toolsets": {"cli": ["override-config"]}}, sort_keys=False),
        encoding="utf-8",
    )
    monkeypatch.setenv("HERMES_CONFIG_PATH", str(override_path))
    monkeypatch.setattr(profiles, "get_active_hermes_home", lambda: active_home)
    cfg.reload_config()

    same = cfg.get_config_for_profile_home(active_home)
    assert same.get("platform_toolsets", {}).get("cli") == ["override-config"]


def test_matching_ambient_home_that_does_not_exist_still_defers_to_get_config(_two_profiles, monkeypatch, tmp_path):
    """Regression (#4516 gate): the nonexistent-home guard must run AFTER the
    ambient-resolver short-circuit. A home that matches the ambient config path
    but whose directory doesn't physically exist (fresh install / monkeypatched
    cfg with no dir on disk) must still defer to get_config() — NOT return {}."""
    cfg, default_home, profile_b_home = _two_profiles
    ghost_ambient = tmp_path / "ghost-ambient"
    monkeypatch.setenv("HERMES_CONFIG_PATH", str(ghost_ambient / "config.yaml"))
    cfg.reload_config()
    assert not ghost_ambient.exists()
    # The requested home matches the (nonexistent) ambient home → must defer to
    # get_config(), honoring the in-memory cfg, not short-circuit to {}.
    result = cfg.get_config_for_profile_home(ghost_ambient)
    assert result == cfg.get_config()


def test_empty_or_none_home_falls_back_to_get_config(_two_profiles):
    """A missing/empty profile home (ImportError fallback path in the worker)
    must not crash — it falls back to the ambient get_config()."""
    cfg, default_home, profile_b_home = _two_profiles
    assert cfg.get_config_for_profile_home(None) == cfg.get_config()
    assert cfg.get_config_for_profile_home("") == cfg.get_config()


def test_nonexistent_profile_home_returns_empty_not_default(_two_profiles):
    """A profile home that diverges from ambient but has no config.yaml yet
    returns an empty dict (the profile's own absent config) rather than silently
    inheriting the default profile's tools."""
    cfg, default_home, profile_b_home = _two_profiles
    ghost = profile_b_home.parent / "ghost"
    result = cfg.get_config_for_profile_home(ghost)
    assert result == {}
    # Critically: it does NOT return the default profile's full toolset list.
    assert result.get("platform_toolsets", {}).get("cli") != [
        "terminal",
        "file",
        "web",
        "search",
        "browser",
    ]
