"""Tests for alive.core.skill_builder."""

import asyncio
import json
import sys
from pathlib import Path
from unittest.mock import AsyncMock

import pytest

from alive.core.skill_builder import (
    BuildResult,
    SkillBuilder,
    _extract_block,
    _safe_eval_dict,
    _safe_parse_json,
    build_skill,
)
from alive.core.skill_registry import SkillRegistry
from alive.llm.base import LLMProvider
from alive.sandbox.dependency import DependencyManager


# ── Fixtures ─────────────────────────────────────────────────────────────────


class MockProvider(LLMProvider):
    """LLM provider that returns canned responses."""

    name = "mock"

    def __init__(self, responses: list[str] | None = None):
        self._responses = list(responses or [])
        self._call_count = 0

    async def complete(self, system_prompt: str, user_prompt: str) -> str:
        if self._call_count < len(self._responses):
            resp = self._responses[self._call_count]
        else:
            resp = self._responses[-1] if self._responses else ""
        self._call_count += 1
        return resp


GOOD_RESPONSE = """Here is the skill:

```name
greeter
```

```description
Greets a person by name.
```

```dependencies
```

```input_schema
{"name": "str"}
```

```output_schema
{"greeting": "str", "success": "bool"}
```

```python skill
\"\"\"Greet a person.\"\"\"


def run(name: str = "World") -> dict:
    return {"greeting": f"Hello, {name}!", "success": True}
```

```python test
from skill import run

def test_greet():
    result = run("Alice")
    assert result["greeting"] == "Hello, Alice!"
    assert result["success"] is True
```

```python sample_input
{"name": "Alice"}
```

```markdown doc
# greeter

Greets a person by name.

## Usage

```python
from skill import run
result = run("Alice")
```
```
"""

BAD_RESPONSE = """
```name
broken
```

```description
Always fails.
```

```dependencies
```

```input_schema
{}
```

```output_schema
{}
```

```python skill
def run() -> dict:
    raise ValueError("I am broken")
    return {"success": True}
```

```python test
from skill import run

def test_broken():
    run()
```

```python sample_input
{}
```

```markdown doc
# broken
```
"""


@pytest.fixture
def registry(tmp_path):
    """Create an isolated registry for testing."""
    import alive.seeds._installer as installer

    original = installer._SEEDS_DIR
    installer._SEEDS_DIR = tmp_path / "empty_seeds"
    installer._SEEDS_DIR.mkdir()
    reg = SkillRegistry(skills_dir=tmp_path / "skills")
    installer._SEEDS_DIR = original
    return reg


@pytest.fixture
def venv_dir(tmp_path):
    """Return a temp venv dir (doesn't create actual venv)."""
    return tmp_path / "venv"


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

    def __init__(self):
        # Skip parent __init__ which imports config
        self._venv_dir = Path(sys.executable).parent.parent

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

    def ensure_venv(self) -> None:
        pass  # system Python already exists

    def install(self, packages: list[str]) -> None:
        pass  # skip pip install in tests


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


# ── Parsing tests ────────────────────────────────────────────────────────────


def test_extract_block_python_label():
    response = "text\n```python skill\ncode here\n```\nmore"
    assert _extract_block("skill", response) == "code here"


def test_extract_block_plain_label():
    response = "text\n```name\nmy_skill\n```\nmore"
    assert _extract_block("name", response) == "my_skill"


def test_extract_block_missing():
    assert _extract_block("missing", "no blocks here") == ""


def test_extract_block_multiline():
    response = "```python skill\nline1\nline2\nline3\n```"
    assert _extract_block("skill", response) == "line1\nline2\nline3"


def test_safe_parse_json_valid():
    assert _safe_parse_json('{"a": 1}') == {"a": 1}


def test_safe_parse_json_python_dict():
    assert _safe_parse_json("{'a': 1}") == {"a": 1}


def test_safe_parse_json_empty():
    assert _safe_parse_json("") == {}


def test_safe_parse_json_invalid():
    assert _safe_parse_json("not json") == {}


def test_safe_eval_dict_valid():
    assert _safe_eval_dict('{"x": "hello"}') == {"x": "hello"}


def test_safe_eval_dict_empty():
    assert _safe_eval_dict("") == {}


def test_safe_eval_dict_invalid():
    assert _safe_eval_dict("not a dict") == {}


# ── BuildResult parsing ─────────────────────────────────────────────────────


def test_parse_well_formed_response(registry):
    builder = SkillBuilder("greet someone", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(GOOD_RESPONSE)
    assert result.skill_name == "greeter"
    assert result.description == "Greets a person by name."
    assert "def run(" in result.code
    assert result.dependencies == []
    assert result.sample_input == {"name": "Alice"}
    assert result.input_schema == {"name": "str"}


def test_parse_missing_code_raises(registry):
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    with pytest.raises(ValueError, match="missing skill code"):
        builder._parse_llm_response("no code blocks here")


def test_parse_name_sanitization(registry):
    response = GOOD_RESPONSE.replace("greeter", "My-Cool Skill!")
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response)
    assert result.skill_name == "my_cool_skill"


def test_derive_name(registry):
    builder = SkillBuilder("fetch the latest stock prices", registry=registry, provider=MockProvider())
    name = builder._derive_name()
    assert "fetch" in name
    assert "latest" in name
    assert "stock" in name


# ── Subprocess testing ───────────────────────────────────────────────────────


def test_test_success(registry, mock_dep_manager, monkeypatch):
    """A passing skill should return success=True."""
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    result = BuildResult(
        skill_name="test_ok",
        description="test",
        code='def run(x: str = "hi") -> dict:\n    return {"result": x, "success": True}\n',
        test_code="",
        dependencies=[],
        input_schema={},
        output_schema={},
        skill_doc="",
        sample_input={"x": "hello"},
    )
    attempt = builder._test(result)
    assert attempt.success is True
    assert attempt.error is None


def test_test_failure(registry, mock_dep_manager, monkeypatch):
    """A failing skill should return success=False."""
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    result = BuildResult(
        skill_name="test_bad",
        description="test",
        code="def run() -> dict:\n    raise ValueError('boom')\n",
        test_code="",
        dependencies=[],
        input_schema={},
        output_schema={},
        skill_doc="",
        sample_input={},
    )
    attempt = builder._test(result)
    assert attempt.success is False
    assert "ValueError" in (attempt.error or attempt.stderr or "")


def test_test_timeout(registry, mock_dep_manager, monkeypatch):
    """A skill that hangs should time out."""
    import alive.sandbox.runner as runner_mod

    original_timeout = runner_mod.DEFAULT_TIMEOUT
    runner_mod.DEFAULT_TIMEOUT = 2  # 2 seconds for test speed
    try:
        builder = SkillBuilder("test", registry=registry, provider=MockProvider())
        monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

        result = BuildResult(
            skill_name="test_hang",
            description="test",
            code="import time\ndef run() -> dict:\n    time.sleep(999)\n    return {}\n",
            test_code="",
            dependencies=[],
            input_schema={},
            output_schema={},
            skill_doc="",
            sample_input={},
        )
        attempt = builder._test(result)
        assert attempt.success is False
        assert "timed out" in (attempt.error or "")
    finally:
        runner_mod.DEFAULT_TIMEOUT = original_timeout


# ── Full pipeline tests ─────────────────────────────────────────────────────


def test_build_full_pipeline(registry, mock_dep_manager, monkeypatch):
    """Full build with mocked LLM — skill should be registered."""
    from rich.console import Console

    # Mock LLM: first call = analysis, second call = generation
    provider = MockProvider(["Analysis: simple greeting skill.", GOOD_RESPONSE])

    builder = SkillBuilder(
        "greet someone",
        console=Console(quiet=True),
        provider=provider,
        registry=registry,
    )
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    info = asyncio.run(builder.build())
    assert info.name == "greeter"
    assert info.description == "Greets a person by name."

    # Verify it was registered
    found = registry.get_skill("greeter")
    assert found.name == "greeter"


def test_build_retry_on_failure(registry, mock_dep_manager, monkeypatch):
    """Build retries when first attempt fails, succeeds on second."""
    from rich.console import Console

    # Responses: analysis, bad code, then good code (fix)
    provider = MockProvider([
        "Analysis: will fail first.",
        BAD_RESPONSE,
        GOOD_RESPONSE,
    ])

    builder = SkillBuilder(
        "test retry",
        console=Console(quiet=True),
        provider=provider,
        registry=registry,
    )
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    info = asyncio.run(builder.build())
    assert info.name == "greeter"
    assert len(builder._attempts) == 2


def test_build_max_attempts_raises(registry, mock_dep_manager, monkeypatch):
    """Build should raise after MAX_ATTEMPTS failures."""
    from rich.console import Console
    import alive.core.skill_builder as sb

    original_max = sb.MAX_ATTEMPTS
    sb.MAX_ATTEMPTS = 2  # Speed up test
    try:
        # All responses produce bad code
        provider = MockProvider(["Analysis.", BAD_RESPONSE, BAD_RESPONSE, BAD_RESPONSE])

        builder = SkillBuilder(
            "always fail",
            console=Console(quiet=True),
            provider=provider,
            registry=registry,
        )
        monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

        with pytest.raises(RuntimeError, match="Failed to build"):
            asyncio.run(builder.build())
    finally:
        sb.MAX_ATTEMPTS = original_max


def test_build_name_collision(registry, mock_dep_manager, monkeypatch):
    """If skill name already exists, append suffix."""
    from rich.console import Console

    # Pre-register a skill named "greeter"
    registry.register(name="greeter", code="def run(): pass", description="existing")

    provider = MockProvider(["Analysis.", GOOD_RESPONSE])

    builder = SkillBuilder(
        "greet",
        console=Console(quiet=True),
        provider=provider,
        registry=registry,
    )
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    info = asyncio.run(builder.build())
    assert info.name == "greeter_2"


# ── Dep manager delegation test ──────────────────────────────────────────────


def test_get_venv_python(registry, mock_dep_manager, monkeypatch):
    """_get_venv_python should return the dep manager's python path."""
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    python_path = builder._get_venv_python()
    assert python_path == Path(sys.executable)


# ── Additional parsing edge cases ───────────────────────────────────────────

from alive.core.skill_builder import BuildAttempt


def test_parse_name_sanitization_special_chars(registry):
    """Name with special characters should sanitize to valid Python identifier."""
    response = GOOD_RESPONSE.replace("greeter", "My-Cool Skill! 2.0 @#$")
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response)
    assert not any(c in result.skill_name for c in "-!@#$. ")


def test_parse_name_all_special_chars_becomes_unnamed(registry):
    """Name that sanitizes to empty should become 'unnamed_skill'."""
    response = GOOD_RESPONSE.replace("greeter", "!!!@@@")
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response)
    assert result.skill_name == "unnamed_skill"


def test_parse_response_no_name_block_derives(registry):
    """When name block is missing, derive from description."""
    response_no_name = GOOD_RESPONSE.replace("```name\ngreeter\n```", "")
    builder = SkillBuilder("fetch the latest stock prices", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response_no_name)
    assert result.skill_name != ""
    assert any(w in result.skill_name for w in ["fetch", "latest", "stock", "prices"])


def test_parse_response_missing_test_generates_default(registry):
    """When test block is missing, auto-generate a placeholder test."""
    response_no_test = GOOD_RESPONSE.replace(
        '```python test\nfrom skill import run\n\ndef test_greet():\n    result = run("Alice")\n    assert result["greeting"] == "Hello, Alice!"\n    assert result["success"] is True\n```',
        "",
    )
    builder = SkillBuilder("greet someone", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response_no_test)
    assert "def test_" in result.test_code
    assert "from skill import run" in result.test_code


def test_parse_response_missing_description_uses_builder_desc(registry):
    """When description block is missing, use the builder's description."""
    response_no_desc = GOOD_RESPONSE.replace("```description\nGreets a person by name.\n```", "")
    builder = SkillBuilder("my custom description", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response_no_desc)
    assert result.description == "my custom description"


def test_derive_name_stop_words_filtered(registry):
    """Stop words should be filtered from derived names."""
    builder = SkillBuilder("a skill to and from the database", registry=registry, provider=MockProvider())
    name = builder._derive_name()
    for stop in ["the", "to", "and", "from", "a"]:
        assert stop not in name.split("_")


def test_derive_name_empty_description(registry):
    """Only-stop-words description should produce 'unnamed_skill'."""
    builder = SkillBuilder("a the and or", registry=registry, provider=MockProvider())
    name = builder._derive_name()
    assert name == "unnamed_skill"


def test_safe_eval_dict_non_dict_returns_empty():
    """_safe_eval_dict with a list literal should return empty dict."""
    assert _safe_eval_dict("[1, 2, 3]") == {}


def test_safe_parse_json_non_dict_returns_empty():
    """_safe_parse_json with valid JSON array should return empty dict."""
    assert _safe_parse_json("[1, 2, 3]") == {}


def test_extract_block_markdown_doc():
    """Extract block with markdown label."""
    response = "text\n```markdown doc\n# Title\n\nContent here.\n```\nmore"
    assert _extract_block("doc", response) == "# Title\n\nContent here."


def test_parse_dependencies_multiline(registry):
    """Dependencies block with multiple packages."""
    response = GOOD_RESPONSE.replace("```dependencies\n```", "```dependencies\nrequests\nbeautifulsoup4\nnumpy\n```")
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response)
    assert result.dependencies == ["requests", "beautifulsoup4", "numpy"]


def test_parse_dependencies_empty_lines(registry):
    """Dependencies block with blank lines should skip them."""
    response = GOOD_RESPONSE.replace("```dependencies\n```", "```dependencies\nrequests\n\n  \nnumpy\n```")
    builder = SkillBuilder("test", registry=registry, provider=MockProvider())
    result = builder._parse_llm_response(response)
    assert result.dependencies == ["requests", "numpy"]


# ── Fix method ──────────────────────────────────────────────────────────────


def test_fix_generates_different_approach_after_3_attempts(registry):
    """After 3 failed attempts, _fix prompt should suggest a different approach."""
    prompts = []

    class CapturingProvider(LLMProvider):
        name = "capture"
        async def complete(self, system_prompt: str, user_prompt: str) -> str:
            prompts.append(user_prompt)
            return GOOD_RESPONSE

    provider = CapturingProvider()
    builder = SkillBuilder("test", registry=registry, provider=provider)
    builder._attempts = [
        BuildAttempt(attempt_number=i, code="x", test_code="y", success=False, error="err")
        for i in range(1, 4)
    ]
    attempt = BuildAttempt(attempt_number=3, code="bad code", test_code="bad test", success=False, error="still broken")

    asyncio.run(builder._fix(BuildResult(
        skill_name="test", description="test", code="bad", test_code="bad",
        dependencies=[], input_schema={}, output_schema={}, skill_doc="", sample_input={},
    ), attempt))
    assert "FUNDAMENTALLY DIFFERENT" in prompts[0]


def test_fix_early_attempts_no_different_approach(registry):
    """Before 3 attempts, fix prompt should NOT suggest different approach."""
    prompts = []

    class CapturingProvider(LLMProvider):
        name = "capture"
        async def complete(self, system_prompt: str, user_prompt: str) -> str:
            prompts.append(user_prompt)
            return GOOD_RESPONSE

    provider = CapturingProvider()
    builder = SkillBuilder("test", registry=registry, provider=provider)
    builder._attempts = [
        BuildAttempt(attempt_number=1, code="x", test_code="y", success=False, error="err")
    ]
    attempt = BuildAttempt(attempt_number=1, code="bad", test_code="bad", success=False, error="err")

    asyncio.run(builder._fix(BuildResult(
        skill_name="test", description="test", code="bad", test_code="bad",
        dependencies=[], input_schema={}, output_schema={}, skill_doc="", sample_input={},
    ), attempt))
    assert "FUNDAMENTALLY DIFFERENT" not in prompts[0]


# ── Name collision ──────────────────────────────────────────────────────────


def test_build_multiple_name_collisions(registry, mock_dep_manager, monkeypatch):
    """Multiple name collisions should increment suffix correctly."""
    from rich.console import Console

    registry.register(name="greeter", code="def run(): pass", description="existing")
    registry.register(name="greeter_2", code="def run(): pass", description="existing")

    provider = MockProvider(["Analysis.", GOOD_RESPONSE])
    builder = SkillBuilder(
        "greet",
        console=Console(quiet=True),
        provider=provider,
        registry=registry,
    )
    monkeypatch.setattr(builder, "_get_dep_manager", lambda: mock_dep_manager)

    info = asyncio.run(builder.build())
    assert info.name == "greeter_3"
