from pathlib import Path

from ididi import Graph
import pytest

from aceai.core.base import AgentBase
from aceai.core.executor import ToolExecutor
from aceai.core.run_state import ToolRunState
from aceai.llm.errors import AceAIConfigurationError
from aceai.llm.interface import UNSET
from aceai.llm.models import LLMToolCall


def write_skill(root: Path, name: str, description: str, body: str) -> Path:
    skill_dir = root / name
    skill_dir.mkdir(parents=True)
    (skill_dir / "SKILL.md").write_text(
        "\n".join(
            [
                "---",
                f"name: {name}",
                f"description: {description}",
                "---",
                body,
            ]
        ),
        encoding="utf-8",
    )
    return skill_dir


def test_build_agent_base(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setenv("HOME", str(tmp_path / "home"))
    agent = AgentBase(
        prompt="You are a helpful assistant.",
        default_model="gpt-4",
        llm_service=None,  # type: ignore
        executor=None,  # type: ignore
        skill_path=tmp_path / "empty-skills",
    )
    assert agent.system_message.content[0]["data"] == "You are a helpful assistant."
    assert agent._default_model == "gpt-4"
    assert agent.max_steps is UNSET
    assert hasattr(agent, "run")


def test_agent_base_add_instruction_updates_system_message(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setenv("HOME", str(tmp_path / "home"))
    agent = AgentBase(
        prompt="Initial",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=None,  # type: ignore[arg-type]
        skill_path=tmp_path / "empty-skills",
    )
    agent.add_instruction(" + More")
    agent.add_instruction(" + More")
    assert agent.system_message.content[0]["data"] == "Initial + More"


def test_agent_base_add_instruction_rejects_empty_string(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    monkeypatch.setenv("HOME", str(tmp_path / "home"))
    agent = AgentBase(
        prompt="Initial",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=None,  # type: ignore[arg-type]
        skill_path=tmp_path / "empty-skills",
    )
    with pytest.raises(ValueError, match="Empty Instruction"):
        agent.add_instruction("")


def test_agent_base_auto_loads_global_and_project_skills(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    home = tmp_path / "home"
    cwd = tmp_path / "project"
    write_skill(home / ".aceai" / "skills", "release", "Release workflow.", "# Release")
    write_skill(cwd / ".agent" / "skills", "review", "Review workflow.", "# Review")
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.chdir(cwd)

    agent = AgentBase(
        prompt="Prompt",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=None,
    )

    assert set(agent.skill_registry.skills) == {"release", "review"}
    system_text = agent.system_message.content[0]["data"]
    assert "<available_skills>" in system_text
    assert "<name>release</name>" in system_text
    assert "<name>review</name>" in system_text


def test_agent_base_filters_enabled_skills(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    home = tmp_path / "home"
    cwd = tmp_path / "project"
    write_skill(home / ".aceai" / "skills", "release", "Release workflow.", "# Release")
    write_skill(cwd / ".agent" / "skills", "review", "Review workflow.", "# Review")
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.chdir(cwd)

    agent = AgentBase(
        prompt="Prompt",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=None,
        enabled_skill_names=("review",),
    )

    system_text = agent.system_message.content[0]["data"]

    assert set(agent.skill_registry.skills) == {"review"}
    assert "<name>review</name>" in system_text
    assert "<name>release</name>" not in system_text


def test_agent_base_disable_skill_path_skips_all_skills(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    home = tmp_path / "home"
    cwd = tmp_path / "project"
    write_skill(home / ".aceai" / "skills", "global", "Global skill.", "# Global")
    write_skill(cwd / ".agent" / "skills", "project", "Project skill.", "# Project")
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.chdir(cwd)

    executor = ToolExecutor(Graph(), [])
    agent = AgentBase(
        prompt="Prompt",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=executor,
        skill_path="disable",
    )

    assert agent.skill_registry.skills == {}
    assert agent.system_message.content[0]["data"] == "Prompt"
    assert "skills_list" not in executor.tools
    assert "skill_view" not in executor.tools


def test_agent_base_explicit_skill_path_skips_project_auto_path(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    home = tmp_path / "home"
    cwd = tmp_path / "project"
    explicit = tmp_path / "explicit-skills"
    write_skill(home / ".aceai" / "skills", "global", "Global skill.", "# Global")
    write_skill(cwd / ".agent" / "skills", "project", "Project skill.", "# Project")
    write_skill(explicit, "explicit", "Explicit skill.", "# Explicit")
    monkeypatch.setenv("HOME", str(home))
    monkeypatch.chdir(cwd)

    agent = AgentBase(
        prompt="Prompt",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=None,
        skill_path=explicit,
    )

    assert set(agent.skill_registry.skills) == {"global", "explicit"}


@pytest.mark.anyio
async def test_agent_base_registers_skill_tools_on_tool_executor(
    tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
    home = tmp_path / "home"
    explicit = tmp_path / "skills"
    write_skill(explicit, "release", "Release workflow.", "# Release\nDo release work.")
    monkeypatch.setenv("HOME", str(home))
    executor = ToolExecutor(Graph(), [])

    AgentBase(
        prompt="Prompt",
        default_model="gpt-4",
        llm_service=None,  # type: ignore[arg-type]
        executor=executor,
        skill_path=explicit,
    )

    assert "skills_list" in executor.tools
    assert "skill_view" in executor.tools
    call = LLMToolCall(
        name="skill_view",
        arguments='{"name":"release"}',
        call_id="call-skill",
    )
    result = await executor.execute(
        executor.resolve_invocation(call),
        tool_state=ToolRunState(),
    )
    assert "Do release work." in result


def test_agent_base_requires_positive_or_unset_max_steps() -> None:
    with pytest.raises(AceAIConfigurationError):
        AgentBase(
            prompt="Prompt",
            default_model="gpt-4",
            llm_service=None,  # type: ignore[arg-type]
            executor=None,  # type: ignore[arg-type]
            max_steps=0,
        )
    with pytest.raises(AceAIConfigurationError):
        AgentBase(
            prompt="Prompt",
            default_model="gpt-4",
            llm_service=None,  # type: ignore[arg-type]
            executor=None,  # type: ignore[arg-type]
            max_steps=-1,
        )


def test_agent_base_rejects_chunk_size_argument() -> None:
    with pytest.raises(TypeError):
        AgentBase(
            prompt="Prompt",
            default_model="gpt-4",
            llm_service=None,  # type: ignore[arg-type]
            executor=None,  # type: ignore[arg-type]
            delta_chunk_size=1,
        )


def test_agent_base_rejects_reasoning_log_argument() -> None:
    with pytest.raises(TypeError):
        AgentBase(
            prompt="Prompt",
            default_model="gpt-4",
            llm_service=None,  # type: ignore[arg-type]
            executor=None,  # type: ignore[arg-type]
            reasoning_log_max_chars=10,
        )
