"""Tests for the .pth startup hook (dependency mode)."""

from __future__ import annotations

import importlib.metadata
import os
import sys
import textwrap
from pathlib import Path
from unittest.mock import patch

from specter_cli._pth_hook import (
    _detect_gpu_from_metadata,
    _detect_gpu_from_pyproject,
    _detect_gpu_type,
    _find_project_root,
    _in_virtualenv,
    _is_pipx_or_uvx,
    _print_status,
    bootstrap,
)


# ---------------------------------------------------------------------------
# Safety checks
# ---------------------------------------------------------------------------


class TestIsPipxOrUvx:
    """Verify pipx/uvx detection guards."""

    def test_pipx_home_env_var(self):
        with patch.dict(os.environ, {"PIPX_HOME": "/home/user/.local/pipx"}):
            assert _is_pipx_or_uvx() is True

    def test_uv_tool_dir_env_var(self):
        with patch.dict(os.environ, {"UV_TOOL_DIR": "/home/user/.local/share/uv/tools"}):
            assert _is_pipx_or_uvx() is True

    def test_pipx_venv_path(self, tmp_path):
        pipx_venvs = tmp_path / ".local" / "pipx" / "venvs"
        pipx_venvs.mkdir(parents=True)
        fake_prefix = pipx_venvs / "specter-host"
        fake_prefix.mkdir()

        with (
            patch.dict(os.environ, {}, clear=False),
            patch("specter_cli._pth_hook.Path.home", return_value=tmp_path),
            patch("specter_cli._pth_hook.sys") as mock_sys,
        ):
            os.environ.pop("PIPX_HOME", None)
            os.environ.pop("UV_TOOL_DIR", None)
            mock_sys.prefix = str(fake_prefix)
            assert _is_pipx_or_uvx() is True

    def test_uvx_tool_path(self, tmp_path):
        uv_tools = tmp_path / ".local" / "share" / "uv" / "tools"
        uv_tools.mkdir(parents=True)
        fake_prefix = uv_tools / "specter-host"
        fake_prefix.mkdir()

        with (
            patch.dict(os.environ, {}, clear=False),
            patch("specter_cli._pth_hook.Path.home", return_value=tmp_path),
            patch("specter_cli._pth_hook.sys") as mock_sys,
        ):
            os.environ.pop("PIPX_HOME", None)
            os.environ.pop("UV_TOOL_DIR", None)
            mock_sys.prefix = str(fake_prefix)
            assert _is_pipx_or_uvx() is True

    def test_normal_venv_not_detected(self, tmp_path):
        with (
            patch.dict(os.environ, {}, clear=False),
            patch("specter_cli._pth_hook.Path.home", return_value=tmp_path),
            patch("specter_cli._pth_hook.sys") as mock_sys,
        ):
            os.environ.pop("PIPX_HOME", None)
            os.environ.pop("UV_TOOL_DIR", None)
            mock_sys.prefix = str(tmp_path / "myproject" / ".venv")
            assert _is_pipx_or_uvx() is False


class TestInVirtualenv:
    """Verify virtualenv detection."""

    def test_base_prefix_differs(self):
        with patch.object(sys, "prefix", "/project/.venv"), \
             patch.object(sys, "base_prefix", "/usr"):
            assert _in_virtualenv() is True

    def test_same_prefix(self):
        # W14 fix: proper test for non-virtualenv detection
        prefix = sys.prefix
        base = sys.base_prefix
        # If we're already in a venv in the test environment, mock both to same value
        with patch.object(sys, "prefix", "/usr"), \
             patch.object(sys, "base_prefix", "/usr"):
            # Also ensure real_prefix is absent
            has_real = hasattr(sys, "real_prefix")
            if has_real:
                saved = sys.real_prefix
                del sys.real_prefix
            try:
                assert _in_virtualenv() is False
            finally:
                if has_real:
                    sys.real_prefix = saved

    def test_real_prefix_set(self):
        with patch.object(sys, "real_prefix", "/usr", create=True):
            assert _in_virtualenv() is True


# ---------------------------------------------------------------------------
# GPU type detection
# ---------------------------------------------------------------------------


class TestDetectGpuFromPyproject:
    """Verify pyproject.toml parsing for GPU extras."""

    def test_finds_h100_extra(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "my-ml-project"
            dependencies = [
                "torch>=2.0",
                "specter-host[h100]>=0.1.8",
            ]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "h100"

    def test_finds_a100_extra(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "my-ml-project"
            dependencies = [
                "specter-host[a100]",
            ]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "a100"

    def test_finds_rtx_pro_6000(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["specter-host[rtx-pro-6000]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "rtx-pro-6000"

    def test_finds_in_dependency_groups(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["torch"]

            [dependency-groups]
            gpu = ["specter-host[l40s]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "l40s"

    def test_finds_in_optional_dependencies(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = []

            [project.optional-dependencies]
            gpu = ["specter-host[4090]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "4090"

    def test_no_specter_host_returns_none(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["torch", "numpy"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) is None

    def test_specter_host_without_extra_returns_none(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["specter-host>=0.1.8"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) is None

    def test_missing_pyproject_returns_none(self, tmp_path):
        assert _detect_gpu_from_pyproject(tmp_path) is None

    def test_invalid_toml_returns_none(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text("this is not valid toml [[[")
        assert _detect_gpu_from_pyproject(tmp_path) is None

    def test_case_insensitive_package_name(self, tmp_path):
        """PEP 503 normalization — underscores and hyphens are equivalent."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["Specter_Host[h100]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "h100"

    def test_multiple_extras_takes_first(self, tmp_path):
        """W13 fix: comma-separated extras like specter-host[h100,a100]."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["specter-host[h100,a100]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "h100"

    def test_multiple_extras_with_spaces(self, tmp_path):
        """W13 fix: extras with spaces like specter-host[h100, a100]."""
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["specter-host[h100, a100]"]
        """))
        assert _detect_gpu_from_pyproject(tmp_path) == "h100"


class TestDetectGpuFromMetadata:
    """Verify marker package detection fallback."""

    def test_finds_installed_marker(self):
        with patch("specter_cli._pth_hook.importlib.metadata.version") as mock_ver:
            def side_effect(name):
                if name == "specter-gpu-a100":
                    return "0.1.0"
                raise importlib.metadata.PackageNotFoundError(name)

            mock_ver.side_effect = side_effect
            assert _detect_gpu_from_metadata() == "a100"

    def test_no_marker_installed(self):
        with patch(
            "specter_cli._pth_hook.importlib.metadata.version",
            side_effect=importlib.metadata.PackageNotFoundError("x"),
        ):
            assert _detect_gpu_from_metadata() is None


class TestDetectGpuType:
    """Verify combined detection logic."""

    def test_pyproject_takes_precedence(self, tmp_path):
        pyproject = tmp_path / "pyproject.toml"
        pyproject.write_text(textwrap.dedent("""\
            [project]
            name = "test"
            dependencies = ["specter-host[h100]"]
        """))
        with patch("specter_cli._pth_hook._find_project_root", return_value=tmp_path):
            assert _detect_gpu_type() == "h100"

    def test_falls_back_to_metadata(self):
        with (
            patch("specter_cli._pth_hook._find_project_root", return_value=None),
            patch("specter_cli._pth_hook._detect_gpu_from_metadata", return_value="l4"),
        ):
            assert _detect_gpu_type() == "l4"

    def test_returns_none_when_nothing_found(self):
        with (
            patch("specter_cli._pth_hook._find_project_root", return_value=None),
            patch("specter_cli._pth_hook._detect_gpu_from_metadata", return_value=None),
        ):
            assert _detect_gpu_type() is None


# ---------------------------------------------------------------------------
# Project root detection
# ---------------------------------------------------------------------------


class TestFindProjectRoot:
    """Verify project root detection from venv location."""

    def test_finds_pyproject_in_venv_parent(self, tmp_path):
        project = tmp_path / "myproject"
        project.mkdir()
        (project / "pyproject.toml").write_text("[project]\nname='test'")
        venv = project / ".venv"
        venv.mkdir()

        with (
            patch.object(sys, "prefix", str(venv)),
            patch.dict(os.environ, {}, clear=False),
        ):
            os.environ.pop("VIRTUAL_ENV", None)
            assert _find_project_root() == project

    def test_finds_pyproject_via_virtual_env(self, tmp_path):
        project = tmp_path / "myproject"
        project.mkdir()
        (project / "pyproject.toml").write_text("[project]\nname='test'")
        venv = project / ".venv"
        venv.mkdir()

        with (
            patch.object(sys, "prefix", "/some/other/path"),
            patch.dict(os.environ, {"VIRTUAL_ENV": str(venv)}),
        ):
            assert _find_project_root() == project

    def test_returns_none_no_pyproject(self, tmp_path):
        venv = tmp_path / ".venv"
        venv.mkdir()

        with (
            patch.object(sys, "prefix", str(venv)),
            patch.dict(os.environ, {}, clear=False),
        ):
            os.environ.pop("VIRTUAL_ENV", None)
            assert _find_project_root() is None


# ---------------------------------------------------------------------------
# Status banner
# ---------------------------------------------------------------------------


class TestPrintStatus:
    """Verify the dim status line output."""

    def test_prints_gpu_and_session(self, capsys):
        _print_status("h100", "s_abc123def456")
        captured = capsys.readouterr()
        assert "h100" in captured.err
        assert "s_abc123def4" in captured.err
        assert "session active" in captured.err

    def test_prints_without_session_id(self, capsys):
        _print_status("a100")
        captured = capsys.readouterr()
        assert "a100" in captured.err
        assert "session active" in captured.err

    def test_uses_dim_ansi(self, capsys):
        _print_status("h100")
        captured = capsys.readouterr()
        assert "\033[2m" in captured.err  # dim
        assert "\033[0m" in captured.err  # reset


# ---------------------------------------------------------------------------
# Bootstrap integration
# ---------------------------------------------------------------------------


class TestBootstrap:
    """Test the main bootstrap entry point."""

    def test_noop_when_not_in_virtualenv(self):
        with patch("specter_cli._pth_hook._in_virtualenv", return_value=False):
            bootstrap()

    def test_noop_when_pipx(self):
        with (
            patch("specter_cli._pth_hook._in_virtualenv", return_value=True),
            patch("specter_cli._pth_hook._is_pipx_or_uvx", return_value=True),
        ):
            bootstrap()

    def test_noop_when_no_gpu_detected(self):
        with (
            patch("specter_cli._pth_hook._in_virtualenv", return_value=True),
            patch("specter_cli._pth_hook._is_pipx_or_uvx", return_value=False),
            patch("specter_cli._pth_hook._detect_gpu_type", return_value=None),
        ):
            bootstrap()

    def test_reuses_existing_matching_session(self, capsys):
        config = {"host": "1.2.3.4", "session_id": "s_123", "gpu_type": "h100"}
        with (
            patch("specter_cli._pth_hook._in_virtualenv", return_value=True),
            patch("specter_cli._pth_hook._is_pipx_or_uvx", return_value=False),
            patch("specter_cli._pth_hook._detect_gpu_type", return_value="h100"),
            patch("specter_cli.config.load_remote_config", return_value=config),
        ):
            bootstrap()
        captured = capsys.readouterr()
        assert "h100" in captured.err
        assert "session active" in captured.err

    def test_warns_on_gpu_mismatch(self, capsys):
        config = {"host": "1.2.3.4", "session_id": "s_123", "gpu_type": "a100"}
        with (
            patch("specter_cli._pth_hook._in_virtualenv", return_value=True),
            patch("specter_cli._pth_hook._is_pipx_or_uvx", return_value=False),
            patch("specter_cli._pth_hook._detect_gpu_type", return_value="h100"),
            patch("specter_cli.config.load_remote_config", return_value=config),
        ):
            bootstrap()
        captured = capsys.readouterr()
        assert "mismatch" in captured.err
        assert "h100" in captured.err

    def test_prompts_login_when_no_auth(self, capsys):
        with (
            patch("specter_cli._pth_hook._in_virtualenv", return_value=True),
            patch("specter_cli._pth_hook._is_pipx_or_uvx", return_value=False),
            patch("specter_cli._pth_hook._detect_gpu_type", return_value="h100"),
            patch("specter_cli.config.load_remote_config", return_value={}),
            patch("specter_cli._pth_hook._check_auth", return_value=None),
        ):
            bootstrap()
        captured = capsys.readouterr()
        assert "SPECTER_API_KEY" in captured.err or "specter login" in captured.err

    def test_reentrant_guard(self):
        """Verify bootstrap doesn't re-enter if already running."""
        with patch.dict(os.environ, {"_SPECTER_PTH_ACTIVE": "1"}):
            bootstrap()

    def test_catches_base_exception(self):
        """W12 fix: bootstrap catches BaseException including KeyboardInterrupt."""
        with patch(
            "specter_cli._pth_hook._in_virtualenv",
            side_effect=KeyboardInterrupt,
        ):
            # Should NOT raise.
            bootstrap()

    def test_catches_system_exit(self):
        """W12 fix: bootstrap catches SystemExit."""
        with patch(
            "specter_cli._pth_hook._in_virtualenv",
            side_effect=SystemExit(1),
        ):
            bootstrap()

    def test_never_raises_on_runtime_error(self):
        """Bootstrap must never raise — even if internals error."""
        with patch(
            "specter_cli._pth_hook._in_virtualenv",
            side_effect=RuntimeError("boom"),
        ):
            bootstrap()

    def test_cleans_up_lock_on_error(self):
        """Lock env var must be cleaned up even if bootstrap errors."""
        with patch("specter_cli._pth_hook._bootstrap_inner", side_effect=RuntimeError("fail")):
            os.environ.pop("_SPECTER_PTH_ACTIVE", None)
            bootstrap()
            assert "_SPECTER_PTH_ACTIVE" not in os.environ

    def test_cleans_up_lock_on_keyboard_interrupt(self):
        """W12: lock cleaned up on KeyboardInterrupt too."""
        with patch(
            "specter_cli._pth_hook._bootstrap_inner",
            side_effect=KeyboardInterrupt,
        ):
            os.environ.pop("_SPECTER_PTH_ACTIVE", None)
            bootstrap()
            assert "_SPECTER_PTH_ACTIVE" not in os.environ


# ---------------------------------------------------------------------------
# Auth detection
# ---------------------------------------------------------------------------


class TestCheckAuth:
    """Test auth detection for the .pth hook."""

    def test_env_var_takes_precedence(self):
        from specter_cli._pth_hook import _check_auth

        with patch.dict(os.environ, {"SPECTER_API_KEY": "sp_live_test123"}):
            assert _check_auth() == "sp_live_test123"

    def test_stored_token_fallback(self):
        from specter_cli._pth_hook import _check_auth

        with (
            patch.dict(os.environ, {}, clear=False),
            patch("specter_cli.config.get_token", return_value="sp_live_stored"),
        ):
            os.environ.pop("SPECTER_API_KEY", None)
            assert _check_auth() == "sp_live_stored"

    def test_no_auth_returns_none(self):
        from specter_cli._pth_hook import _check_auth

        with (
            patch.dict(os.environ, {}, clear=False),
            patch("specter_cli.config.get_token", return_value=None),
        ):
            os.environ.pop("SPECTER_API_KEY", None)
            assert _check_auth() is None
