"""Tests for alive.sandbox — runner and dependency manager."""

import asyncio
import sys
from pathlib import Path

import pytest

from alive.sandbox.dependency import DependencyManager
from alive.sandbox.runner import SkillResult, run_skill, run_skill_code


# ── Helpers ──────────────────────────────────────────────────────────────────


class MockDependencyManager(DependencyManager):
    """Uses system Python — no real venv needed."""

    def __init__(self):
        self._venv_dir = Path(sys.executable).parent.parent

    @property
    def python(self) -> Path:
        return Path(sys.executable)

    def ensure_venv(self) -> None:
        pass

    def install(self, packages: list[str]) -> None:
        pass


@pytest.fixture
def mock_dm():
    return MockDependencyManager()


# ── run_skill_code tests ────────────────────────────────────────────────────


def test_run_skill_code_success(mock_dm):
    """Simple skill that returns a dict should succeed."""
    code = 'def run(name: str = "World") -> dict:\n    return {"greeting": f"Hello, {name}!"}\n'
    result = run_skill_code(code, {"name": "Alice"}, dep_manager=mock_dm)
    assert result.success is True
    assert result.output == {"greeting": "Hello, Alice!"}
    assert result.error is None
    assert result.duration_ms >= 0


def test_run_skill_code_no_input(mock_dm):
    """Skill with defaults should work without explicit input."""
    code = 'def run() -> dict:\n    return {"status": "ok"}\n'
    result = run_skill_code(code, dep_manager=mock_dm)
    assert result.success is True
    assert result.output == {"status": "ok"}


def test_run_skill_code_failure(mock_dm):
    """Skill that raises should return success=False."""
    code = 'def run() -> dict:\n    raise ValueError("broken")\n'
    result = run_skill_code(code, {}, dep_manager=mock_dm)
    assert result.success is False
    assert result.error is not None
    assert "ValueError" in result.error


def test_run_skill_code_non_dict_return(mock_dm):
    """Skill returning non-dict should fail."""
    code = 'def run() -> dict:\n    return "not a dict"\n'
    result = run_skill_code(code, {}, dep_manager=mock_dm)
    assert result.success is False
    assert "must return a dict" in (result.error or "")


def test_run_skill_code_timeout(mock_dm):
    """Skill that hangs should time out."""
    code = "import time\ndef run() -> dict:\n    time.sleep(999)\n    return {}\n"
    result = run_skill_code(code, {}, timeout=2, dep_manager=mock_dm)
    assert result.success is False
    assert "timed out" in (result.error or "").lower()


def test_run_skill_code_syntax_error(mock_dm):
    """Skill with syntax error should fail."""
    code = "def run() -> dict:\n    return {bad syntax\n"
    result = run_skill_code(code, {}, dep_manager=mock_dm)
    assert result.success is False
    assert result.error is not None


# ── run_skill tests ─────────────────────────────────────────────────────────


def test_run_skill_missing(tmp_path, mock_dm):
    """Running a skill that doesn't exist should fail gracefully."""
    result = run_skill("nonexistent_skill", skills_dir=tmp_path, dep_manager=mock_dm)
    assert result.success is False
    assert "not found" in (result.error or "").lower()


def test_run_skill_from_directory(tmp_path, mock_dm):
    """Run a skill from its directory."""
    skill_dir = tmp_path / "my_skill"
    skill_dir.mkdir()
    (skill_dir / "skill.py").write_text(
        'def run(x: int = 1) -> dict:\n    return {"doubled": x * 2}\n'
    )
    result = run_skill("my_skill", {"x": 5}, skills_dir=tmp_path, dep_manager=mock_dm)
    assert result.success is True
    assert result.output == {"doubled": 10}


# ── DependencyManager tests ─────────────────────────────────────────────────


def test_dep_manager_python_path(tmp_path):
    """Python path should be platform-correct."""
    dm = DependencyManager(venv_dir=tmp_path / "venv")
    if sys.platform == "win32":
        assert dm.python == tmp_path / "venv" / "Scripts" / "python.exe"
    else:
        assert dm.python == tmp_path / "venv" / "bin" / "python"


def test_dep_manager_venv_dir(tmp_path):
    """venv_dir property should return the configured path."""
    dm = DependencyManager(venv_dir=tmp_path / "my_venv")
    assert dm.venv_dir == tmp_path / "my_venv"


# ── _parse_error tests ────────────────────────────────────────────────────


from alive.sandbox.runner import _parse_error


def test_parse_error_json_with_error_key():
    """Should extract error from JSON stdout."""
    stdout = '{"success": false, "error": "ValueError: boom"}'
    result = _parse_error(stdout, "")
    assert result == "ValueError: boom"


def test_parse_error_json_without_error_key():
    """JSON without error key should fall through to stderr."""
    stdout = '{"success": true}'
    result = _parse_error(stdout, "some stderr")
    assert result == "some stderr"


def test_parse_error_invalid_json_fallback_stderr():
    """Non-JSON stdout should fall through to stderr."""
    result = _parse_error("not json", "ImportError: no module")
    assert result == "ImportError: no module"


def test_parse_error_empty_both():
    """Both empty should return 'Unknown error'."""
    result = _parse_error("", "")
    assert result == "Unknown error"


def test_parse_error_stdout_fallback():
    """No stderr: should fall back to raw stdout."""
    result = _parse_error("raw output text", "")
    assert result == "raw output text"


# ── Additional run_skill_code tests ──────────────────────────────────────────


def test_run_skill_code_import_error(mock_dm):
    """Skill that imports nonexistent module should fail."""
    code = "import nonexistent_module_xyz123\ndef run() -> dict:\n    return {}\n"
    result = run_skill_code(code, {}, dep_manager=mock_dm)
    assert result.success is False
    assert "nonexistent_module" in (result.error or "")


def test_run_skill_code_with_complex_input(mock_dm):
    """Skill should handle complex input data."""
    code = '''
def run(data: dict = None, items: list = None) -> dict:
    data = data or {}
    items = items or []
    return {"count": len(items), "keys": list(data.keys())}
'''
    result = run_skill_code(
        code,
        {"data": {"a": 1, "b": 2}, "items": [1, 2, 3]},
        dep_manager=mock_dm,
    )
    assert result.success is True
    assert result.output["count"] == 3
    assert set(result.output["keys"]) == {"a", "b"}


def test_run_skill_code_duration_positive(mock_dm):
    """Duration should be positive even for fast skills."""
    code = 'def run() -> dict:\n    return {"ok": True}\n'
    result = run_skill_code(code, dep_manager=mock_dm)
    assert result.duration_ms >= 0


# ── Additional run_skill tests ──────────────────────────────────────────────


def test_run_skill_with_metadata(tmp_path, mock_dm):
    """Skill with metadata.json should load and run."""
    skill_dir = tmp_path / "meta_skill"
    skill_dir.mkdir()
    (skill_dir / "skill.py").write_text(
        'def run(x: int = 1) -> dict:\n    return {"result": x * 3}\n'
    )
    (skill_dir / "metadata.json").write_text(json.dumps({
        "name": "meta_skill",
        "description": "test",
        "dependencies": [],
    }))
    result = run_skill("meta_skill", {"x": 7}, skills_dir=tmp_path, dep_manager=mock_dm)
    assert result.success is True
    assert result.output["result"] == 21


def test_run_skill_with_bad_metadata(tmp_path, mock_dm):
    """Skill with corrupt metadata.json should still run (best-effort deps)."""
    skill_dir = tmp_path / "bad_meta"
    skill_dir.mkdir()
    (skill_dir / "skill.py").write_text(
        'def run() -> dict:\n    return {"ok": True}\n'
    )
    (skill_dir / "metadata.json").write_text("not valid json")
    result = run_skill("bad_meta", {}, skills_dir=tmp_path, dep_manager=mock_dm)
    assert result.success is True
    assert result.output == {"ok": True}


def test_run_skill_no_metadata(tmp_path, mock_dm):
    """Skill without metadata.json should still run."""
    skill_dir = tmp_path / "no_meta"
    skill_dir.mkdir()
    (skill_dir / "skill.py").write_text(
        'def run() -> dict:\n    return {"bare": True}\n'
    )
    result = run_skill("no_meta", {}, skills_dir=tmp_path, dep_manager=mock_dm)
    assert result.success is True
    assert result.output == {"bare": True}


import json

# ── Integration test: build → run ──────────────────────────────────────────


def test_build_then_run_integration(tmp_path, mock_dm):
    """Integration: build a skill via SkillBuilder, then run via sandbox executor."""
    from rich.console import Console

    from alive.core.skill_builder import SkillBuilder
    from alive.core.skill_registry import SkillRegistry
    import alive.seeds._installer as installer

    # Isolated registry
    original_seeds = installer._SEEDS_DIR
    installer._SEEDS_DIR = tmp_path / "empty_seeds"
    installer._SEEDS_DIR.mkdir()

    try:
        skills_dir = tmp_path / "skills"
        registry = SkillRegistry(skills_dir=skills_dir)

        # Mock LLM that returns a working skill
        class FixedProvider:
            name = "mock"

            async def complete(self, system_prompt: str, user_prompt: str) -> str:
                if "Analyze" in system_prompt or "analyzer" in system_prompt:
                    return "Analysis: simple math doubler skill."
                return """\
```name
doubler
```

```description
Doubles a number.
```

```dependencies
```

```input_schema
{"x": "int"}
```

```output_schema
{"result": "int", "success": "bool"}
```

```python skill
def run(x: int = 1) -> dict:
    return {"result": x * 2, "success": True}
```

```python test
from skill import run

def test_double():
    assert run(5)["result"] == 10
```

```python sample_input
{"x": 7}
```

```markdown doc
# doubler
Doubles a number.
```
"""

        builder = SkillBuilder(
            "double a number",
            console=Console(quiet=True),
            provider=FixedProvider(),
            registry=registry,
        )

        # Use mock dep manager for both builder and runner
        from unittest.mock import patch
        with patch.object(builder, "_get_dep_manager", return_value=mock_dm):
            info = asyncio.run(builder.build())

        assert info.name == "doubler"
        assert info.description == "Doubles a number."

        # Verify skill was saved to disk
        skill_file = skills_dir / "doubler" / "skill.py"
        assert skill_file.exists()

        # Phase 2: Run the saved skill via the sandbox executor
        result = run_skill("doubler", {"x": 21}, skills_dir=skills_dir, dep_manager=mock_dm)
        assert result.success is True
        assert result.output["result"] == 42
        assert result.output["success"] is True

    finally:
        installer._SEEDS_DIR = original_seeds
