"""Tests for system.bash RPC handler."""

import asyncio
import os
import subprocess
import sys
import tempfile
import pytest
from pathlib import Path

from skop_runner.message_handler import MessageHandler, MAX_CONCURRENT_BASH
from skop_runner.file_manager import FileManager
from skop_runner.mock_backend import MockKernelBackend


@pytest.fixture
def tmp_project(tmp_path):
    """Create a temp project directory."""
    return tmp_path


@pytest.fixture
def handler(tmp_project):
    """Create a MessageHandler with mock backend and temp project root."""
    backend = MockKernelBackend()
    fm = FileManager()
    fm.set_root(str(tmp_project))
    h = MessageHandler(kernel_backend=backend, file_manager=fm)
    return h


async def bash_request(handler, command, timeout=None):
    """Helper to send a system.bash JSON-RPC request."""
    params = {"command": command}
    if timeout is not None:
        params["timeout"] = timeout
    msg = {"jsonrpc": "2.0", "id": "test-1", "method": "system.bash", "params": params}
    return await handler.handle_message(msg)


@pytest.mark.asyncio
async def test_basic_echo(handler):
    """Basic command: echo hello → stdout='hello\\n', exit_code=0."""
    resp = await bash_request(handler, "echo hello")
    assert "result" in resp
    result = resp["result"]
    assert result["stdout"].strip() == "hello"
    assert result["exit_code"] == 0
    assert result["truncated"] is False


@pytest.mark.asyncio
async def test_failing_command(handler):
    """Failing command: exit 1 → exit_code=1."""
    resp = await bash_request(handler, "exit 1")
    result = resp["result"]
    assert result["exit_code"] == 1


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_stderr_output(handler):
    """Stderr output: echo err >&2 → stderr contains 'err'."""
    resp = await bash_request(handler, "echo err >&2")
    result = resp["result"]
    assert "err" in result["stderr"]


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_timeout(handler):
    """Timeout: sleep 999 with timeout=1 → exit_code=-1."""
    resp = await bash_request(handler, "sleep 999", timeout=1)
    result = resp["result"]
    assert result["exit_code"] == -1
    assert "timed out" in result["stderr"]


@pytest.mark.asyncio
async def test_no_project_root():
    """No project root set → ValueError."""
    backend = MockKernelBackend()
    fm = FileManager()
    # Don't set root
    h = MessageHandler(kernel_backend=backend, file_manager=fm)
    resp = await bash_request(h, "echo hello")
    assert "error" in resp
    assert "project root" in resp["error"]["message"].lower()


@pytest.mark.asyncio
async def test_concurrency_limit(handler):
    """Exceeding MAX_CONCURRENT_BASH raises RuntimeError."""
    # Fill up the bash slots
    handler._active_bash_count = MAX_CONCURRENT_BASH

    resp = await bash_request(handler, "echo hello")
    assert "error" in resp
    assert "concurrent" in resp["error"]["message"].lower()

    # Reset
    handler._active_bash_count = 0


@pytest.mark.asyncio
async def test_invalid_command_empty(handler):
    """Empty command string → ValueError."""
    resp = await bash_request(handler, "")
    assert "error" in resp
    assert "non-empty" in resp["error"]["message"].lower()


@pytest.mark.asyncio
async def test_invalid_command_whitespace(handler):
    """Whitespace-only command → ValueError."""
    resp = await bash_request(handler, "   ")
    assert "error" in resp
    assert "non-empty" in resp["error"]["message"].lower()


@pytest.mark.asyncio
async def test_clean_environment(handler):
    """Environment is clean: env output should not contain ANTHROPIC_API_KEY."""
    # Set a fake secret in the current process env
    os.environ["ANTHROPIC_API_KEY"] = "sk-test-secret-key"
    try:
        resp = await bash_request(handler, "env")
        result = resp["result"]
        assert "ANTHROPIC_API_KEY" not in result["stdout"]
        assert "sk-test-secret-key" not in result["stdout"]
    finally:
        del os.environ["ANTHROPIC_API_KEY"]


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_cwd_is_project_root(handler, tmp_project):
    """Commands run with cwd set to project root."""
    resp = await bash_request(handler, "pwd")
    result = resp["result"]
    # Resolve symlinks (macOS /tmp → /private/tmp)
    assert result["stdout"].strip() == str(tmp_project.resolve())


@pytest.mark.asyncio
async def test_bash_count_decrements_on_success(handler):
    """Active bash count returns to 0 after successful command."""
    assert handler._active_bash_count == 0
    await bash_request(handler, "echo hello")
    assert handler._active_bash_count == 0


@pytest.mark.asyncio
async def test_bash_count_decrements_on_timeout(handler):
    """Active bash count returns to 0 after timeout."""
    assert handler._active_bash_count == 0
    await bash_request(handler, "sleep 999", timeout=1)
    assert handler._active_bash_count == 0


# ── Venv activation in bash ──────────────────────────────────────────────


@pytest.fixture
def venv_project(tmp_path):
    """Create a temp project with a real Python venv."""
    subprocess.run(
        [sys.executable, "-m", "venv", str(tmp_path / ".venv")],
        check=True,
    )
    return tmp_path


@pytest.fixture
def handler_with_venv(venv_project):
    """MessageHandler whose project root contains a .venv."""
    backend = MockKernelBackend()
    fm = FileManager()
    fm.set_root(str(venv_project))
    h = MessageHandler(kernel_backend=backend, file_manager=fm)
    return h


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_pip_resolves_to_venv(handler_with_venv, venv_project):
    """'which pip' should resolve to the project venv, not system."""
    resp = await bash_request(handler_with_venv, "which pip")
    result = resp["result"]
    assert result["exit_code"] == 0
    pip_path = result["stdout"].strip()
    # Must be inside the project's .venv
    assert str(venv_project.resolve()) in str(Path(pip_path).resolve())


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_python_resolves_to_venv(handler_with_venv, venv_project):
    """'which python' should resolve to the project venv."""
    resp = await bash_request(handler_with_venv, "which python")
    result = resp["result"]
    assert result["exit_code"] == 0
    python_path = result["stdout"].strip()
    # Don't resolve() — venv python is a symlink to the real binary
    assert ".venv/bin/python" in python_path


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_virtual_env_set_in_bash(handler_with_venv, venv_project):
    """VIRTUAL_ENV env var should point at the project venv."""
    resp = await bash_request(handler_with_venv, "echo $VIRTUAL_ENV")
    result = resp["result"]
    venv_dir = result["stdout"].strip()
    assert str(venv_project.resolve()) in str(Path(venv_dir).resolve())


@pytest.mark.asyncio
async def test_pip_install_targets_venv(handler_with_venv, venv_project):
    """pip install should install into the project venv, not system."""
    # Install a small, harmless package
    resp = await bash_request(handler_with_venv, "pip install six")
    result = resp["result"]
    assert result["exit_code"] == 0

    # Verify it was installed in the venv's site-packages
    resp2 = await bash_request(handler_with_venv, "python -c \"import six; print(six.__file__)\"")
    result2 = resp2["result"]
    assert result2["exit_code"] == 0
    assert str(venv_project.resolve()) in str(Path(result2["stdout"].strip()).resolve())


@pytest.mark.skipif(sys.platform == "win32", reason="Unix-only shell commands")
@pytest.mark.asyncio
async def test_session_python_path_takes_priority(tmp_path):
    """When a kernel session has python_path, its bin/ is used over detected envs."""
    # Create two venvs — one in the project (.venv) and one outside
    subprocess.run([sys.executable, "-m", "venv", str(tmp_path / ".venv")], check=True)
    external_venv = tmp_path / "external-env"
    subprocess.run([sys.executable, "-m", "venv", str(external_venv)], check=True)

    backend = MockKernelBackend()
    fm = FileManager()
    fm.set_root(str(tmp_path))
    h = MessageHandler(kernel_backend=backend, file_manager=fm)

    # Register a session pointing at the external venv
    external_python = str(external_venv / "bin" / "python")
    from skop_runner.session_manager import NotebookSession
    from datetime import datetime, timezone
    h.session_manager._sessions["test.ipynb"] = NotebookSession(
        session_id="sess-1",
        kernel_id="kern-1",
        notebook_path="test.ipynb",
        created_at=datetime.now(timezone.utc),
        python_path=external_python,
    )

    resp = await bash_request(h, "which python")
    result = resp["result"]
    python_path = result["stdout"].strip()
    # Don't resolve() — venv python is a symlink to the real binary
    assert "external-env/bin/python" in python_path


@pytest.mark.asyncio
async def test_no_venv_falls_back_gracefully(handler):
    """Without a venv, bash still works (no crash, pip may not be found)."""
    resp = await bash_request(handler, "echo works")
    result = resp["result"]
    assert result["exit_code"] == 0
    assert result["stdout"].strip() == "works"
