"""Tests for the skill installer — validate and hot-load generated skills."""

import os

import pytest
import yaml

from fliiq.runtime.skills.base import SkillBase
from fliiq.runtime.skills.installer import (
    SkillInstallError,
    install_skill,
    validate_skill_directory,
)
from fliiq.runtime.agent.tools import ToolRegistry


def _make_skill(
    tmp_path: str,
    name: str,
    return_value: str = "ok",
    description: str = "Test skill",
    credentials: list | None = None,
) -> str:
    """Create a minimal skill directory with SKILL.md + fliiq.yaml + main.py."""
    skill_dir = os.path.join(tmp_path, name)
    os.makedirs(skill_dir, exist_ok=True)

    with open(os.path.join(skill_dir, "SKILL.md"), "w") as f:
        f.write(f'---\nname: {name}\ndescription: "{description}"\n---\n\nInstructions.\n')

    config = {
        "input_schema": {
            "type": "object",
            "properties": {
                "value": {"type": "string", "description": "A test value"},
            },
        },
    }
    if credentials:
        config["credentials"] = credentials
    with open(os.path.join(skill_dir, "fliiq.yaml"), "w") as f:
        yaml.dump(config, f)

    with open(os.path.join(skill_dir, "main.py"), "w") as f:
        f.write(f"async def handler(params):\n    return {{'result': '{return_value}'}}\n")

    return skill_dir


def test_validate_valid_skill(tmp_path):
    """Well-formed skill directory passes validation."""
    skill_dir = _make_skill(str(tmp_path), "good_skill")
    info = validate_skill_directory(tmp_path / "good_skill")
    assert info["name"] == "good_skill"
    assert info["description"] == "Test skill"
    assert isinstance(info["skill"], SkillBase)


def test_validate_missing_skill_md(tmp_path):
    """Missing SKILL.md raises SkillInstallError."""
    skill_dir = tmp_path / "no_md"
    skill_dir.mkdir()
    (skill_dir / "fliiq.yaml").write_text("input_schema:\n  type: object\n  properties: {}\n")
    (skill_dir / "main.py").write_text("async def handler(params): return {}\n")

    with pytest.raises(SkillInstallError, match="Missing SKILL.md"):
        validate_skill_directory(skill_dir)


def test_validate_missing_fliiq_yaml(tmp_path):
    """Missing fliiq.yaml raises SkillInstallError."""
    skill_dir = tmp_path / "no_yaml"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text('---\nname: test\ndescription: "test"\n---\n')
    (skill_dir / "main.py").write_text("async def handler(params): return {}\n")

    with pytest.raises(SkillInstallError, match="Missing fliiq.yaml"):
        validate_skill_directory(skill_dir)


def test_validate_missing_main_py(tmp_path):
    """Missing main.py raises SkillInstallError."""
    skill_dir = tmp_path / "no_main"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text('---\nname: test\ndescription: "test"\n---\n')
    (skill_dir / "fliiq.yaml").write_text("input_schema:\n  type: object\n  properties: {}\n")

    with pytest.raises(SkillInstallError, match="Missing main.py"):
        validate_skill_directory(skill_dir)


def test_validate_bad_frontmatter(tmp_path):
    """Malformed SKILL.md frontmatter raises SkillInstallError."""
    skill_dir = tmp_path / "bad_md"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text("No frontmatter here.\n")
    (skill_dir / "fliiq.yaml").write_text("input_schema:\n  type: object\n  properties: {}\n")
    (skill_dir / "main.py").write_text("async def handler(params): return {}\n")

    with pytest.raises(SkillInstallError, match="Invalid SKILL.md"):
        validate_skill_directory(skill_dir)


def test_validate_bad_python(tmp_path):
    """Syntax error in main.py raises SkillInstallError."""
    skill_dir = _make_skill(str(tmp_path), "bad_py")
    # Overwrite with bad syntax
    with open(os.path.join(str(tmp_path), "bad_py", "main.py"), "w") as f:
        f.write("def handler(params)\n    this is not valid python\n")

    with pytest.raises(SkillInstallError, match="Failed to load skill"):
        validate_skill_directory(tmp_path / "bad_py")


def test_validate_bad_schema(tmp_path):
    """fliiq.yaml with non-object input_schema raises SkillInstallError."""
    skill_dir = tmp_path / "bad_schema"
    skill_dir.mkdir()
    (skill_dir / "SKILL.md").write_text('---\nname: test\ndescription: "test"\n---\n')
    (skill_dir / "fliiq.yaml").write_text("input_schema:\n  type: string\n")
    (skill_dir / "main.py").write_text("async def handler(params): return {}\n")

    with pytest.raises(SkillInstallError, match="input_schema must have type: object"):
        validate_skill_directory(skill_dir)


def test_install_registers_tool(tmp_path):
    """install_skill adds the skill to the ToolRegistry."""
    _make_skill(str(tmp_path), "new_tool")
    registry = ToolRegistry()

    result = install_skill(tmp_path / "new_tool", registry)

    assert "installed successfully" in result
    tool_names = [d["name"] for d in registry.get_tool_definitions()]
    assert "new_tool" in tool_names


def test_install_name_collision(tmp_path):
    """install_skill rejects duplicate tool names."""
    _make_skill(str(tmp_path), "duplicate")
    registry = ToolRegistry()

    # Install first time — success
    install_skill(tmp_path / "duplicate", registry)

    # Install again — collision
    _make_skill(str(tmp_path / "other"), "duplicate")
    with pytest.raises(SkillInstallError, match="already exists"):
        install_skill(tmp_path / "other" / "duplicate", registry)


def test_install_credentials_message(tmp_path):
    """Success message lists required credentials."""
    _make_skill(str(tmp_path), "cred_skill", credentials=["SPOTIFY_API_KEY", "SPOTIFY_SECRET"])
    registry = ToolRegistry()

    result = install_skill(tmp_path / "cred_skill", registry)

    assert "installed successfully" in result
    assert "SPOTIFY_API_KEY" in result
    assert "SPOTIFY_SECRET" in result


@pytest.mark.asyncio
async def test_installed_skill_executes(tmp_path):
    """An installed skill can be executed through the registry."""
    _make_skill(str(tmp_path), "exec_skill", return_value="works")
    registry = ToolRegistry()

    install_skill(tmp_path / "exec_skill", registry)
    result = await registry.execute("exec_skill", {"value": "test"})

    assert "works" in result


def _make_skill_with_test_example(tmp_path, name="tested_skill"):
    """Create a skill directory with test_example in fliiq.yaml."""
    skill_dir = os.path.join(str(tmp_path), name)
    os.makedirs(skill_dir, exist_ok=True)

    with open(os.path.join(skill_dir, "SKILL.md"), "w") as f:
        f.write(f'---\nname: {name}\ndescription: "Skill with test"\n---\n\nInstructions.\n')

    config = {
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {"type": "string", "description": "Search query"},
            },
        },
        "test_example": {
            "params": {"query": "python programming"},
            "expect_keys": ["results", "count"],
        },
    }
    with open(os.path.join(skill_dir, "fliiq.yaml"), "w") as f:
        yaml.dump(config, f)

    with open(os.path.join(skill_dir, "main.py"), "w") as f:
        f.write("async def handler(params):\n    return {'results': [], 'count': 0}\n")

    return skill_dir


def test_install_test_example_in_message(tmp_path):
    """Install message includes mandatory test instructions when test_example exists."""
    _make_skill_with_test_example(tmp_path)
    registry = ToolRegistry()

    result = install_skill(tmp_path / "tested_skill", registry)

    assert "MANDATORY" in result
    assert "test_main.py" in result
    assert "python programming" in result
    assert "results" in result
    assert "count" in result


def test_install_no_test_example_no_test_instructions(tmp_path):
    """Install message omits test instructions when test_example is absent (backward compat)."""
    _make_skill(str(tmp_path), "old_skill")
    registry = ToolRegistry()

    result = install_skill(tmp_path / "old_skill", registry)

    assert "installed successfully" in result
    assert "MANDATORY" not in result


def test_test_example_accessible_on_skill_base(tmp_path):
    """test_example is accessible as a property on SkillBase."""
    _make_skill_with_test_example(tmp_path)
    info = validate_skill_directory(tmp_path / "tested_skill")

    assert info["test_example"] is not None
    assert info["test_example"]["params"] == {"query": "python programming"}
    assert info["test_example"]["expect_keys"] == ["results", "count"]
    assert info["skill"].test_example == info["test_example"]


def test_test_example_none_for_old_skills(tmp_path):
    """Skills without test_example have None for the property."""
    _make_skill(str(tmp_path), "legacy_skill")
    info = validate_skill_directory(tmp_path / "legacy_skill")

    assert info["test_example"] is None
    assert info["skill"].test_example is None
