"""
Module: test_dev
Description: Tests for development session command

Implements:
    - docs/cli/reference.md#huitzo-dev
"""

from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch

import httpx
import pytest
from fastapi import HTTPException
from pydantic import BaseModel, ValidationError as PydanticValidationError
from rich.console import Console

from huitzo_cli.docs_server.server import DocsServer
from huitzo_cli.errors import (
    CommandNotFoundError,
    CommandValidationError,
    HuitzoCliError,
)
from huitzo_cli.repl.client import _check_response
from huitzo_cli.repl.formatter import OutputFormatter
from huitzo_cli.sandbox.proxy import (
    LocalSandbox,
    StubContext,
    StubService,
    _format_validation_error,
)


def test_local_sandbox_init() -> None:
    """Test LocalSandbox initializes with empty state."""
    sandbox = LocalSandbox()
    assert sandbox._commands == {}
    assert sandbox._metadata == {}


def test_local_sandbox_discover_commands() -> None:
    """Test discover_commands scans entry_points."""
    sandbox = LocalSandbox()
    # Won't find any commands in test env, but should not error
    commands = sandbox.discover_commands()
    assert isinstance(commands, dict)


def test_docs_server_init() -> None:
    """Test docs server initialization."""
    server = DocsServer(port=8124)
    assert server.port == 8124
    assert not server.is_running()


def test_docs_server_manifest() -> None:
    """Test getting MCP manifest."""
    server = DocsServer()
    manifest = server.get_mcp_manifest()

    assert "name" in manifest
    assert "version" in manifest
    assert "endpoints" in manifest


def test_docs_server_ensure_docs(temp_dir: Path) -> None:
    """Test docs directory creation."""
    docs_dir = temp_dir / "docs"

    server = DocsServer(docs_dir=docs_dir)

    # Mock git to avoid actual clone
    with patch("subprocess.run") as mock_run:
        mock_run.return_value.returncode = 0

        # Create the directory to avoid clone attempt
        docs_dir.mkdir(parents=True, exist_ok=True)

        result = server.ensure_docs()
        assert result.exists()


def test_docs_server_bind_localhost(temp_dir: Path) -> None:
    """Test docs server binds to 127.0.0.1 (C5)."""
    docs_dir = temp_dir / "docs"
    docs_dir.mkdir(parents=True, exist_ok=True)

    server = DocsServer(docs_dir=docs_dir, port=0)

    with (
        patch("huitzo_cli.docs_server.server.subprocess.run"),
        patch("huitzo_cli.docs_server.server.subprocess.Popen") as mock_popen,
    ):
        mock_popen.return_value = MagicMock()
        server.start()

        # Check that --bind 127.0.0.1 is in the Popen args
        call_args = mock_popen.call_args[0][0]
        assert "--bind" in call_args
        bind_idx = call_args.index("--bind")
        assert call_args[bind_idx + 1] == "127.0.0.1"


def test_docs_server_stop_without_start() -> None:
    """Test stopping server when not running."""
    server = DocsServer()
    server.stop()  # Should not raise error


# --- Sandbox execution tests ---


@pytest.mark.asyncio
async def test_sandbox_execute_success() -> None:
    """Test executing a discovered command."""
    sandbox = LocalSandbox()

    async def fake_cmd(args, ctx):
        return {"result": args.get("name", "world")}

    sandbox._commands["greet"] = fake_cmd
    sandbox._metadata["greet"] = {"name": "greet", "namespace": "test/greet"}

    result = await sandbox.execute("greet", {"name": "Alice"})
    assert result == {"result": "Alice"}


@pytest.mark.asyncio
async def test_sandbox_execute_missing_command() -> None:
    """Test executing a non-existent command raises KeyError."""
    sandbox = LocalSandbox()

    with pytest.raises(KeyError, match="Command not found: nope"):
        await sandbox.execute("nope", {})


@pytest.mark.asyncio
async def test_sandbox_execute_command_error() -> None:
    """Test executing a command that raises propagates the exception."""
    sandbox = LocalSandbox()

    async def failing_cmd(args, ctx):
        raise ValueError("bad input")

    sandbox._commands["fail"] = failing_cmd
    sandbox._metadata["fail"] = {"name": "fail"}

    with pytest.raises(ValueError, match="bad input"):
        await sandbox.execute("fail", {})


def test_sandbox_discovery_failures() -> None:
    """Test that discovery failures are tracked."""
    sandbox = LocalSandbox()
    # After discover, failures should be empty (no broken entry_points in test env)
    sandbox.discover_commands()
    assert isinstance(sandbox.get_discovery_failures(), list)


def test_sandbox_auth_token_stored() -> None:
    """Test that auth token is stored on LocalSandbox."""
    sandbox = LocalSandbox(auth_token="my-token")
    assert sandbox._auth_token == "my-token"


def test_sandbox_stop_without_start() -> None:
    """Test stopping sandbox when not started."""
    sandbox = LocalSandbox()
    sandbox.stop()  # Should not raise


def test_sandbox_tls_failure_is_hard_error() -> None:
    """Test TLS failure raises RuntimeError instead of falling back (C11)."""
    sandbox = LocalSandbox(auth_token="test-token")

    with patch(
        "huitzo_cli.sandbox.proxy._generate_self_signed_cert",
        side_effect=RuntimeError("cert gen failed"),
    ):
        with pytest.raises(RuntimeError, match="TLS certificate generation failed"):
            sandbox.start(port=0, use_tls=True)


def test_sandbox_auto_generates_auth_token() -> None:
    """Test sandbox auto-generates auth token when none provided (W23)."""
    sandbox = LocalSandbox(auth_token=None)

    with patch("huitzo_cli.sandbox.proxy._generate_self_signed_cert") as mock_cert:
        mock_cert.return_value = ("/tmp/cert.pem", "/tmp/key.pem")
        with patch("uvicorn.Server") as mock_server_cls:
            mock_instance = MagicMock()
            mock_instance.started = True
            mock_instance.servers = []
            mock_server_cls.return_value = mock_instance

            sandbox.start(port=9999, use_tls=True)

    # auth_token should have been auto-generated
    assert sandbox._auth_token is not None
    assert len(sandbox._auth_token) == 64  # hex(32) = 64 chars


# --- _verify_token security tests ---


def _make_request(auth_header: str | None = None) -> MagicMock:
    """Create a mock Request with optional Authorization header."""
    request = MagicMock()
    headers: dict[str, str] = {}
    if auth_header is not None:
        headers["Authorization"] = auth_header
    request.headers = headers
    return request


def test_verify_token_rejects_missing_header() -> None:
    """Test that missing Authorization header returns 401."""
    sandbox = LocalSandbox(auth_token="secret-token")
    request = _make_request()

    with pytest.raises(HTTPException) as exc_info:
        sandbox._verify_token(request)
    assert exc_info.value.status_code == 401
    assert "Missing" in str(exc_info.value.detail)


def test_verify_token_rejects_invalid_token() -> None:
    """Test that wrong Bearer token returns 401."""
    sandbox = LocalSandbox(auth_token="secret-token")
    request = _make_request("Bearer wrong-token")

    with pytest.raises(HTTPException) as exc_info:
        sandbox._verify_token(request)
    assert exc_info.value.status_code == 401
    assert "Invalid" in str(exc_info.value.detail)


def test_verify_token_accepts_valid_token() -> None:
    """Test that correct Bearer token passes."""
    sandbox = LocalSandbox(auth_token="secret-token")
    request = _make_request("Bearer secret-token")

    # Should not raise
    sandbox._verify_token(request)


def test_verify_token_skips_when_no_auth_configured() -> None:
    """Test that verification is skipped when no auth_token is set."""
    sandbox = LocalSandbox(auth_token=None)
    request = _make_request()  # No header at all

    # Should not raise
    sandbox._verify_token(request)


def test_classify_discovery_failure_module_not_found() -> None:
    """Test enhanced error message for ModuleNotFoundError."""
    sandbox = LocalSandbox()

    error = ModuleNotFoundError("No module named 'storecheck_claims.commands'")
    result = sandbox._classify_discovery_failure("storecheck", error)

    # Should contain error information (at minimum the basic format)
    assert "storecheck" in result
    assert "storecheck_claims.commands" in result


# --- StubService tests ---


def test_stub_service_raises_attribute_error() -> None:
    """Test StubService raises AttributeError with service name in message."""
    stub = StubService("ctx.llm")

    with pytest.raises(AttributeError, match="ctx.llm") as exc_info:
        stub.generate("hello")
    assert "not available in sandbox mode" in str(exc_info.value)


def test_stub_service_message_includes_guidance() -> None:
    """Test StubService error includes actionable guidance."""
    stub = StubService("ctx.http")

    with pytest.raises(AttributeError) as exc_info:
        stub.get("https://example.com")
    msg = str(exc_info.value)
    assert "huitzo pack dev --use-platform" in msg
    assert "ANTHROPIC_API_KEY" in msg


# --- StubContext tests ---


def test_stub_context_has_all_service_stubs() -> None:
    """Test StubContext provides stub services for all platform services."""
    ctx = StubContext()

    for service_name in ("llm", "http", "email", "telegram", "files", "ssh"):
        stub = getattr(ctx, service_name)
        assert isinstance(stub, StubService), f"ctx.{service_name} is not a StubService"


def test_stub_context_has_deployment_mode() -> None:
    """Test StubContext provides deployment_mode."""
    ctx = StubContext()
    assert ctx.deployment_mode == "sandbox"


# --- _format_validation_error tests ---


def test_format_validation_error_missing_fields() -> None:
    """Test formatting Pydantic validation errors for missing required fields."""

    class SampleArgs(BaseModel):
        name: str
        age: int

    try:
        SampleArgs(**{})
    except PydanticValidationError as e:
        result = _format_validation_error(e, "test-command")

    assert "test-command" in result
    assert "name" in result
    assert "age" in result
    assert "describe test-command" in result


def test_format_validation_error_wrong_type() -> None:
    """Test formatting Pydantic validation errors for wrong types."""

    class SampleArgs(BaseModel):
        count: int

    try:
        SampleArgs(count="not_a_number")
    except PydanticValidationError as e:
        result = _format_validation_error(e, "my-cmd")

    assert "my-cmd" in result
    assert "count" in result


# --- Platform context integration tests ---


@pytest.mark.asyncio
async def test_sandbox_execute_with_platform_context() -> None:
    """Test that use_platform=True creates a real SDK Context."""
    sandbox = LocalSandbox(use_platform=True)

    async def check_ctx(args, ctx):
        # Should be a real SDK Context, not a StubContext
        from huitzo_sdk import Context

        assert isinstance(ctx, Context)
        assert ctx.command_name == "check"
        return {"ok": True}

    sandbox._commands["check"] = check_ctx
    sandbox._metadata["check"] = {"name": "check", "namespace": "test/check"}

    result = await sandbox.execute("check", {})
    assert result == {"ok": True}


@pytest.mark.asyncio
async def test_sandbox_execute_without_platform_stays_stub() -> None:
    """Test that use_platform=False (default) uses StubContext."""
    sandbox = LocalSandbox(use_platform=False)

    async def check_ctx(args, ctx):
        assert isinstance(ctx, StubContext)
        return {"stub": True}

    sandbox._commands["check"] = check_ctx
    sandbox._metadata["check"] = {"name": "check", "namespace": "test/check"}

    result = await sandbox.execute("check", {})
    assert result == {"stub": True}


# --- Session heartbeat integration tests ---


def test_dev_heartbeat_initialized_after_sandbox_start() -> None:
    """Test that SessionHeartbeat is created after sandbox starts."""
    with (
        patch("huitzo_cli.commands.dev.ConfigManager") as mock_config_cls,
        patch("huitzo_cli.commands.dev.AuthClient") as mock_auth_cls,
        patch("huitzo_cli.commands.dev.PackValidator") as mock_validator_cls,
        patch("huitzo_cli.commands.dev.subprocess.run") as mock_pip,
        patch("huitzo_cli.sandbox.proxy.LocalSandbox") as mock_sandbox_cls,
        patch("huitzo_cli.session_heartbeat.SessionHeartbeat") as mock_hb_cls,
    ):
        # Setup mocks
        mock_auth = MagicMock()
        mock_auth.validate_token.return_value = (True, "")
        mock_auth.get_token.return_value = "test-token"
        mock_auth.get_api_url.return_value = "http://localhost:8000"
        mock_auth_cls.return_value = mock_auth

        mock_config_cls.return_value = MagicMock()

        mock_validator = MagicMock()
        mock_validator.validate.return_value = MagicMock(valid=True, errors=[])
        mock_validator_cls.return_value = mock_validator

        mock_pip.return_value = MagicMock(returncode=0)

        mock_sandbox = MagicMock()
        mock_sandbox.discover_commands.return_value = {
            "greet": {"namespace": "@test/pack/greet", "description": "Greet user"}
        }
        mock_sandbox.get_discovery_failures.return_value = []
        mock_sandbox.start.return_value = "https://localhost:8080"
        mock_sandbox._auth_token = "test-token"
        mock_sandbox._ca_cert_path = None
        mock_sandbox_cls.return_value = mock_sandbox

        mock_hb = MagicMock()
        mock_hb.register.return_value = "session-123"
        mock_hb_cls.return_value = mock_hb

        # Import and run (will hit KeyboardInterrupt in REPL)
        from huitzo_cli.commands.dev import dev

        with patch("huitzo_cli.commands.dev.asyncio.run", side_effect=KeyboardInterrupt):
            try:
                dev(docs=False, port=8080, repl=True, no_tls=True)
            except SystemExit:
                pass

        # Verify heartbeat was created and started
        mock_hb_cls.assert_called_once()
        mock_hb.register.assert_called_once()
        mock_hb.stop.assert_called_once()


# --- Error types tests ---


def test_command_validation_error_carries_detail() -> None:
    """Test CommandValidationError preserves error detail."""
    err = CommandValidationError("field 'name' is required")
    assert err.detail == "field 'name' is required"
    assert err.user_message == "field 'name' is required"
    assert isinstance(err, HuitzoCliError)


def test_command_not_found_error_carries_name() -> None:
    """Test CommandNotFoundError preserves command name."""
    err = CommandNotFoundError("my-command")
    assert err.command_name == "my-command"
    assert "my-command" in str(err)
    assert isinstance(err, HuitzoCliError)


# --- REPL client _check_response tests ---


def _mock_response(status_code: int, json_data: dict[str, Any] | None = None) -> httpx.Response:
    """Create a mock httpx.Response."""
    response = httpx.Response(
        status_code=status_code,
        json=json_data,
        request=httpx.Request("GET", "http://test"),
    )
    return response


def test_check_response_success() -> None:
    """Test _check_response passes on 2xx."""
    resp = _mock_response(200, {"ok": True})
    _check_response(resp)  # Should not raise


def test_check_response_422_raises_validation_error() -> None:
    """Test _check_response raises CommandValidationError on 422."""
    resp = _mock_response(422, {"detail": "field 'name' is required"})
    with pytest.raises(CommandValidationError) as exc_info:
        _check_response(resp)
    assert "name" in exc_info.value.detail


def test_check_response_404_raises_not_found() -> None:
    """Test _check_response raises CommandNotFoundError on 404."""
    resp = _mock_response(404, {"detail": "Command not found: foo"})
    with pytest.raises(CommandNotFoundError):
        _check_response(resp)


def test_check_response_401_raises_auth_error() -> None:
    """Test _check_response raises AuthenticationError on 401."""
    from huitzo_cli.errors import AuthenticationError

    resp = _mock_response(401, {"detail": "Unauthorized"})
    with pytest.raises(AuthenticationError):
        _check_response(resp)


def test_check_response_500_raises_server_error() -> None:
    """Test _check_response raises ServerError on 5xx."""
    from huitzo_cli.errors import ServerError

    resp = _mock_response(500, {"detail": "Internal server error"})
    with pytest.raises(ServerError):
        _check_response(resp)


# --- Formatter tests ---


def test_format_command_detail_renders_args_schema() -> None:
    """Test format_command_detail shows args schema when present."""
    console = Console(file=MagicMock(), force_terminal=True, width=120)
    formatter = OutputFormatter(console)

    cmd: dict[str, Any] = {
        "name": "compare-visits",
        "namespace": "claims/compare-visits",
        "timeout": 60,
        "queue": "default",
        "description": "Compare visit records",
        "args_schema": {
            "properties": {
                "store_id": {"type": "string", "description": "Store identifier"},
                "date": {"type": "string", "description": "Visit date"},
            },
            "required": ["store_id"],
        },
    }

    # Should not raise
    formatter.format_command_detail(cmd)

    # Verify console.print was called (schema table rendered)
    assert console.file.write.called


def test_format_validation_error_shows_detail() -> None:
    """Test format_validation_error displays detail text."""
    console = Console(file=MagicMock(), force_terminal=True, width=120)
    formatter = OutputFormatter(console)

    formatter.format_validation_error("field 'name' is required")
    assert console.file.write.called


def test_format_command_not_found_with_suggestions() -> None:
    """Test format_command_not_found shows suggestions."""
    console = Console(file=MagicMock(), force_terminal=True, width=120)
    formatter = OutputFormatter(console)

    formatter.format_command_not_found("comp", ["compare-visits", "hello", "compute"])
    assert console.file.write.called


# --- Global exception handler tests ---


def test_safe_main_catches_exceptions() -> None:
    """Test _safe_main catches exceptions and doesn't leak tracebacks."""
    from huitzo_cli.main import _safe_main

    with patch("huitzo_cli.main.app", side_effect=RuntimeError("boom")):
        with pytest.raises(SystemExit) as exc_info:
            _safe_main()
        assert exc_info.value.code == 1


def test_safe_main_passes_system_exit() -> None:
    """Test _safe_main re-raises SystemExit."""
    from huitzo_cli.main import _safe_main

    with patch("huitzo_cli.main.app", side_effect=SystemExit(0)):
        with pytest.raises(SystemExit) as exc_info:
            _safe_main()
        assert exc_info.value.code == 0
