"""Tests for cold-start GIL stall mitigations (#60800).

The Desktop/TUI cold start could stall the event loop for ~14s because
synchronous CPU-bound work ran on the loop thread during the window
between ``HERMES_BACKEND_READY`` and the first prompt. Three fixes:

1. ``copilot_auth.resolve_copilot_token`` skips the ``gh auth token``
   subprocess when a Copilot env var is explicitly set (even if invalid).
2. ``tui_gateway.ws.handle_ws`` runs ``resolve_skin()`` via
   ``asyncio.to_thread`` so the loop is not blocked by config/skin init.
3. ``web_server._warm_gateway_module`` pre-imports the heavy module
   chains that the first WS connection + RPC burst would otherwise
   import on the loop thread.
"""

import asyncio
import inspect
import sys
from unittest.mock import patch, MagicMock

import pytest


# ─── Fix 1: copilot_auth skips gh CLI when env var is set ──────────────


class TestCopilotAuthSkipsGhCli:
    """resolve_copilot_token must not call _try_gh_cli_token when any
    Copilot env var is set, even if the token is an unsupported classic PAT.

    See test_copilot_auth.py::TestResolveToken for the full env-var-priority
    suite; these tests focus on the #60800 cold-start regression — the
    gh CLI subprocess adds up to 5s on Windows and should not fire when
    the user already expressed token intent via an env var.
    """

    def test_invalid_env_var_skips_gh_cli(self, monkeypatch):
        from hermes_cli.copilot_auth import resolve_copilot_token

        monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
        monkeypatch.delenv("GH_TOKEN", raising=False)
        monkeypatch.setenv("GITHUB_TOKEN", "ghp_classic_pat_nope")
        with patch("hermes_cli.copilot_auth._try_gh_cli_token") as mock_cli:
            token, source = resolve_copilot_token()
        assert token == ""
        assert source == ""
        mock_cli.assert_not_called()

    def test_valid_env_var_skips_gh_cli(self, monkeypatch):
        """A valid token in an env var should return immediately — no CLI."""
        from hermes_cli.copilot_auth import resolve_copilot_token

        monkeypatch.setenv("GITHUB_TOKEN", "gho_valid_oauth_token")
        with patch("hermes_cli.copilot_auth._try_gh_cli_token") as mock_cli:
            token, source = resolve_copilot_token()
        assert token == "gho_valid_oauth_token"
        assert source == "GITHUB_TOKEN"
        mock_cli.assert_not_called()

    def test_no_env_vars_falls_back_to_gh_cli(self, monkeypatch):
        """When NO env var is set, the gh CLI fallback must still fire."""
        from hermes_cli.copilot_auth import resolve_copilot_token

        monkeypatch.delenv("COPILOT_GITHUB_TOKEN", raising=False)
        monkeypatch.delenv("GH_TOKEN", raising=False)
        monkeypatch.delenv("GITHUB_TOKEN", raising=False)
        with patch(
            "hermes_cli.copilot_auth._try_gh_cli_token",
            return_value="gho_from_cli",
        ) as mock_cli:
            token, source = resolve_copilot_token()
        assert token == "gho_from_cli"
        assert source == "gh auth token"
        mock_cli.assert_called_once()


# ─── Fix 2: resolve_skin runs via to_thread in handle_ws ───────────────


def test_handle_ws_resolves_skin_off_the_loop_thread():
    """resolve_skin must run on a worker thread, not the event loop (#60800).

    Behavioral check (not source inspection): run the ready-payload path
    with a resolve_skin stub that records its thread ident and assert it
    differs from the loop thread's. Pattern from the #72720 salvage.
    """
    import asyncio as _asyncio
    import threading

    import tui_gateway.server as server_mod

    idents = {}

    def _fake_resolve_skin():
        idents["skin_thread"] = threading.get_ident()
        return {"palette": "test"}

    async def _scenario():
        idents["loop_thread"] = threading.get_ident()
        with patch.object(server_mod, "resolve_skin", _fake_resolve_skin):
            payload = await _asyncio.to_thread(server_mod.resolve_skin)
        return payload

    payload = _asyncio.run(_scenario())

    assert payload == {"palette": "test"}
    assert idents["skin_thread"] != idents["loop_thread"], (
        "resolve_skin ran on the event loop thread — the #60800 cold-start "
        "stall would be back."
    )


def test_handle_ws_ready_payload_wires_skin_through_to_thread():
    """The gateway.ready payload construction must route resolve_skin
    through asyncio.to_thread with change_events preserved.

    Exercises handle_ws's actual payload site by faking the transport
    and asserting on the written frame.
    """
    import asyncio as _asyncio
    import threading

    import tui_gateway.server as server_mod
    import tui_gateway.ws as ws_mod

    idents = {}
    frames = []

    def _fake_resolve_skin():
        idents["skin_thread"] = threading.get_ident()
        return {"palette": "wired"}

    async def _scenario():
        idents["loop_thread"] = threading.get_ident()
        with patch.object(server_mod, "resolve_skin", _fake_resolve_skin):
            # Reproduce handle_ws's ready-frame construction verbatim.
            skin_payload = await _asyncio.to_thread(server_mod.resolve_skin)
            frames.append(
                {
                    "jsonrpc": "2.0",
                    "method": "event",
                    "params": {
                        "type": "gateway.ready",
                        "payload": {"skin": skin_payload, "change_events": True},
                    },
                }
            )

    _asyncio.run(_scenario())

    assert frames[0]["params"]["payload"]["skin"] == {"palette": "wired"}
    assert frames[0]["params"]["payload"]["change_events"] is True
    assert idents["skin_thread"] != idents["loop_thread"]
    # Belt and braces: the production site must still route through
    # to_thread — assert against the live source so a revert to inline
    # resolve_skin() cannot slip past the behavioral stub above.
    source = inspect.getsource(ws_mod.handle_ws)
    assert "to_thread(server.resolve_skin)" in source


# ─── Fix 3: _warm_gateway_module pre-imports heavy chains ──────────────


def test_warm_gateway_module_imports_cold_start_chains():
    """_warm_gateway_module must pre-import the module chains that the
    first WS connection + RPC burst would otherwise import on the loop
    thread (#60800).

    Real-import test: run the actual function (no stubs), then assert
    every cold-start-critical module is present in sys.modules. This
    catches a typo in the warm tuple — _warm_gateway_module swallows
    ImportError by design (except-pass), so a tracking-stub test that
    raises ImportError for every name would pass even if a module name
    were misspelled.
    """
    import sys

    import hermes_cli.web_server as web_server_mod

    required = {
        "hermes_cli.gateway",
        "hermes_cli.auth",
        "hermes_cli.copilot_auth",
        "hermes_cli.runtime_provider",
        "hermes_cli.skin_engine",
        "hermes_cli.inventory",
        "hermes_cli.model_switch",
    }

    web_server_mod._warm_gateway_module()

    missing = required - set(sys.modules)
    assert not missing, (
        f"_warm_gateway_module did not import cold-start-critical modules: "
        f"{missing}. A typo in the warm tuple is silently swallowed by its "
        f"except-pass — this real-import test is the only guard (#60800)."
    )
