"""Step definitions for Agent Skills discovery tests."""

from __future__ import annotations

import shutil
import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock

from behave import given, then, when
from behave.runner import Context

from cleveragents.application.services.config_service import ConfigService
from cleveragents.skills.discovery import (
    DiscoveredAgentSkill,
    build_tool_spec,
    discover_agent_skills,
    parse_agent_skills_paths,
    register_discovered_skills,
    scan_directory,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runtime import ToolSpec

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _create_skill_md(folder: Path, name: str, description: str) -> None:
    """Create a SKILL.md with YAML front-matter in *folder*."""
    folder.mkdir(parents=True, exist_ok=True)
    content = (
        "---\n"
        f"name: {name}\n"
        f"description: {description}\n"
        "---\n"
        f"\n# {name}\n\n"
        f"{description}\n"
    )
    (folder / "SKILL.md").write_text(content, encoding="utf-8")


def _noop_handler(**_kwargs: Any) -> dict[str, Any]:
    return {"status": "test"}


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("the agent skills discovery module is available")
def step_discovery_module_available(context: Context) -> None:
    """Verify imports are accessible."""
    assert parse_agent_skills_paths is not None
    assert scan_directory is not None
    assert discover_agent_skills is not None


# ---------------------------------------------------------------------------
# Config key
# ---------------------------------------------------------------------------


@when('I check the config registry for "{key}"')
def step_check_config_registry(context: Context, key: str) -> None:
    context.config_entry = ConfigService.get_entry(key)


@then("the config entry should exist")
def step_config_entry_exists(context: Context) -> None:
    assert context.config_entry is not None, "Config entry not found"


@then('the config entry type should be "{type_name}"')
def step_config_entry_type(context: Context, type_name: str) -> None:
    entry = context.config_entry
    assert entry is not None
    assert entry.python_type.__name__ == type_name


@then('the config entry env var should be "{env_var}"')
def step_config_entry_env_var(context: Context, env_var: str) -> None:
    entry = context.config_entry
    assert entry is not None
    assert entry.env_var == env_var


# ---------------------------------------------------------------------------
# Path parsing
# ---------------------------------------------------------------------------


@when('I parse agent skills paths "{raw_paths}"')
def step_parse_paths(context: Context, raw_paths: str) -> None:
    context.parsed_paths = parse_agent_skills_paths(raw_paths)


@when("I parse an empty agent skills path string")
def step_parse_empty_paths(context: Context) -> None:
    context.parsed_paths = parse_agent_skills_paths("")


@then("the result should contain {count:d} path")
def step_path_count_singular(context: Context, count: int) -> None:
    assert len(context.parsed_paths) == count


@then("the result should contain {count:d} paths")
def step_path_count(context: Context, count: int) -> None:
    assert len(context.parsed_paths) == count


# ---------------------------------------------------------------------------
# Discovery from directories
# ---------------------------------------------------------------------------


@given("a temporary directory with agent skill folders")
def step_temp_dir_with_skills(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))

    assert context.table is not None, "Expected table data"
    for row in context.table:
        folder_name = row["folder_name"]
        skill_name = row["skill_name"]
        description = row["description"]
        if skill_name == "_none_":
            # Write SKILL.md without a name field
            folder = tmpdir / folder_name
            folder.mkdir(parents=True, exist_ok=True)
            content = f"---\ndescription: {description}\n---\n\n# {folder_name}\n"
            (folder / "SKILL.md").write_text(content, encoding="utf-8")
        else:
            _create_skill_md(tmpdir / folder_name, skill_name, description)


@given("a temporary directory with some folders missing SKILL.md")
def step_temp_dir_no_skill_md(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    # Create folder without SKILL.md
    (tmpdir / "no-skill-folder").mkdir()
    # Create a plain file (not a folder)
    (tmpdir / "plain-file.txt").write_text("not a skill", encoding="utf-8")


@given("a temporary directory with a SKILL.md that has no front-matter")
def step_temp_dir_no_front_matter(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "bad-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text("# No front-matter here\n", encoding="utf-8")


@when("I scan the directory for agent skills")
def step_scan_directory(context: Context) -> None:
    context.discovered = scan_directory(context.tmpdir)


@then("I should discover {count:d} agent skill")
def step_discover_count_singular(context: Context, count: int) -> None:
    assert len(context.discovered) == count, (
        f"Expected {count}, got {len(context.discovered)}"
    )


@then("I should discover {count:d} agent skills")
def step_discover_count(context: Context, count: int) -> None:
    assert len(context.discovered) == count, (
        f"Expected {count}, got {len(context.discovered)}"
    )


@then('the discovered skill "{name}" should have description "{description}"')
def step_discovered_skill_description(
    context: Context, name: str, description: str
) -> None:
    skill = next((s for s in context.discovered if s.name == name), None)
    assert skill is not None, f"Skill '{name}' not found"
    assert skill.description == description


@then("the discovered skill should use folder name as name")
def step_discovered_skill_folder_name(context: Context) -> None:
    assert len(context.discovered) == 1
    skill = context.discovered[0]
    # When no name in front-matter, it falls back to folder name
    assert skill.name == "fallback-tool"


# ---------------------------------------------------------------------------
# Non-existent directory
# ---------------------------------------------------------------------------


@when("I run discovery on a non-existent directory")
def step_discover_nonexistent(context: Context) -> None:
    context.discovery_result = discover_agent_skills(
        [Path("/nonexistent/path/that/does/not/exist")]
    )


@then("the discovery result should have {count:d} discovered skills")
def step_discovery_result_count(context: Context, count: int) -> None:
    assert len(context.discovery_result.discovered) == count


@then('the discovery result should have errors mentioning "{text}"')
def step_discovery_result_errors(context: Context, text: str) -> None:
    errors_joined = " ".join(context.discovery_result.errors)
    assert text in errors_joined, f"Expected '{text}' in errors, got: {errors_joined}"


# ---------------------------------------------------------------------------
# ToolSpec building
# ---------------------------------------------------------------------------


@given('a discovered agent skill named "{name}" at "{path}"')
def step_given_discovered_skill(context: Context, name: str, path: str) -> None:
    context.discovered_skill = DiscoveredAgentSkill(
        name=name,
        description=f"Test skill {name}",
        path=path,
    )


@when("I build a ToolSpec from the discovered skill")
def step_build_tool_spec(context: Context) -> None:
    context.tool_spec = build_tool_spec(context.discovered_skill)


@then('the ToolSpec name should be "{expected}"')
def step_tool_spec_name(context: Context, expected: str) -> None:
    assert context.tool_spec.name == expected


@then('the ToolSpec source should be "{expected}"')
def step_tool_spec_source(context: Context, expected: str) -> None:
    assert context.tool_spec.source == expected


@then('the ToolSpec source_metadata should contain path "{expected}"')
def step_tool_spec_source_metadata_path(context: Context, expected: str) -> None:
    assert context.tool_spec.source_metadata.get("path") == expected


# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------


@given("a ToolRegistry with no existing tools")
def step_empty_tool_registry(context: Context) -> None:
    context.tool_registry = ToolRegistry()


@given("a list of {count:d} discovered agent skills")
def step_discovered_skills_list(context: Context, count: int) -> None:
    context.discovered_skills = [
        DiscoveredAgentSkill(
            name=f"tool-{i}",
            description=f"Test tool {i}",
            path=f"/tmp/skills/tool-{i}",
        )
        for i in range(count)
    ]


@given('a ToolRegistry with an existing tool "{name}"')
def step_registry_with_tool(context: Context, name: str) -> None:
    context.tool_registry = ToolRegistry()
    existing_spec = ToolSpec(
        name=name,
        description="Existing tool",
        handler=_noop_handler,
    )
    context.tool_registry.register(existing_spec)


@given('a discovered agent skill named "{name}"')
def step_single_discovered_skill(context: Context, name: str) -> None:
    context.discovered_skills = [
        DiscoveredAgentSkill(
            name=name,
            description=f"Discovered {name}",
            path=f"/tmp/skills/{name}",
        )
    ]


@when('I register discovered skills with "{strategy}" conflict strategy')
def step_register_discovered(context: Context, strategy: str) -> None:
    try:
        registered, conflicts = register_discovered_skills(
            context.discovered_skills,
            context.tool_registry,
            on_conflict=strategy,
        )
        context.registered_tools = registered
        context.registration_conflicts = conflicts
        context.registration_error = None
    except ValueError as exc:
        context.registration_error = exc
        context.registered_tools = []
        context.registration_conflicts = []


@then("{count:d} tools should be registered in the ToolRegistry")
def step_registered_tool_count(context: Context, count: int) -> None:
    assert len(context.registered_tools) == count, (
        f"Expected {count}, got {len(context.registered_tools)}"
    )


@then("{count:d} tool should be registered in the ToolRegistry")
def step_registered_tool_count_singular(context: Context, count: int) -> None:
    assert len(context.registered_tools) == count, (
        f"Expected {count}, got {len(context.registered_tools)}"
    )


@then("{count:d} conflicts should be reported")
def step_conflict_count(context: Context, count: int) -> None:
    assert len(context.registration_conflicts) == count


@then('{count:d} conflict should be reported with tool name "{name}"')
def step_conflict_with_name(context: Context, count: int, name: str) -> None:
    assert len(context.registration_conflicts) == count
    if count > 0:
        assert context.registration_conflicts[0].tool_name == name


@then('a discovery ValueError should be raised mentioning "{text}"')
def step_value_error_raised_discovery(context: Context, text: str) -> None:
    assert context.registration_error is not None, "Expected ValueError not raised"
    assert text in str(context.registration_error), (
        f"Expected '{text}' in error message, got: {context.registration_error}"
    )


@then('the tool "{name}" should have source "{expected_source}"')
def step_tool_has_source(context: Context, name: str, expected_source: str) -> None:
    spec = context.tool_registry.get(name)
    assert spec is not None, f"Tool '{name}' not found in registry"
    assert spec.source == expected_source


# ---------------------------------------------------------------------------
# Refresh
# ---------------------------------------------------------------------------


@given("a SkillRegistryService with a ToolRegistry")
def step_service_with_registry(context: Context) -> None:
    from cleveragents.application.services.skill_registry_service import (
        SkillRegistryService,
    )

    context.tool_registry = ToolRegistry()
    # Create a mock skill_repo since we only need discovery features
    mock_repo = MagicMock()
    context.skill_registry_service = SkillRegistryService(
        skill_repo=mock_repo,
        tool_registry=context.tool_registry,
    )


@given("agent skills paths pointing to a directory with {count:d} skills")
def step_paths_with_skills(context: Context, count: int) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    for i in range(count):
        _create_skill_md(tmpdir / f"skill-{i}", f"skill-{i}", f"Skill {i}")
    context.agent_skills_raw_paths = str(tmpdir)


@when("I call discover_and_register")
def step_call_discover_and_register(context: Context) -> None:
    context.discovery_result = context.skill_registry_service.discover_and_register(
        context.agent_skills_raw_paths
    )


@then("{count:d} agent skills should be registered")
def step_agent_skills_registered(context: Context, count: int) -> None:
    registered = context.skill_registry_service.registered_agent_tools
    assert len(registered) == count, f"Expected {count}, got {len(registered)}"


@when("the directory now has {count:d} skills and I call refresh_agent_skills")
def step_add_more_and_refresh(context: Context, count: int) -> None:
    # Add more skills to the directory
    existing = list(context.tmpdir.iterdir())
    existing_count = len([d for d in existing if d.is_dir()])
    for i in range(existing_count, count):
        _create_skill_md(context.tmpdir / f"skill-{i}", f"skill-{i}", f"Skill {i}")
    context.discovery_result = context.skill_registry_service.refresh_agent_skills(
        context.agent_skills_raw_paths
    )


# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------


@when("I register an empty discovered skills list")
def step_register_empty_list(context: Context) -> None:
    registered, conflicts = register_discovered_skills(
        [],
        context.tool_registry,
    )
    context.registered_tools = registered
    context.registration_conflicts = conflicts


@then(
    "the registration should return {spec_count:d} specs and {conflict_count:d} conflicts"
)
def step_registration_returns(
    context: Context, spec_count: int, conflict_count: int
) -> None:
    assert len(context.registered_tools) == spec_count
    assert len(context.registration_conflicts) == conflict_count


@when("I scan a non-directory path for agent skills")
def step_scan_non_dir(context: Context) -> None:
    tmpfile = Path(tempfile.mktemp(suffix=".txt"))
    tmpfile.write_text("not a directory", encoding="utf-8")
    context.add_cleanup(lambda: tmpfile.unlink(missing_ok=True))
    context.discovered = scan_directory(tmpfile)


@when("I run discovery on a path that is a file")
def step_discover_file_path(context: Context) -> None:
    tmpfile = Path(tempfile.mktemp(suffix=".txt"))
    tmpfile.write_text("not a directory", encoding="utf-8")
    context.add_cleanup(lambda: tmpfile.unlink(missing_ok=True))
    context.discovery_result = discover_agent_skills([tmpfile])


@given("a temporary directory with a SKILL.md that has invalid YAML")
def step_temp_dir_invalid_yaml(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "bad-yaml-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text(
        "---\ninvalid: [yaml: {broken\n---\n",
        encoding="utf-8",
    )


@given("a temporary directory with a SKILL.md that has empty front-matter")
def step_temp_dir_empty_fm(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "empty-fm-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text(
        "---\n\n---\n\n# Empty FM\n",
        encoding="utf-8",
    )


@given("a temporary directory with a SKILL.md that has non-dict YAML")
def step_temp_dir_non_dict_yaml(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "list-yaml-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text(
        "---\n- item1\n- item2\n---\n\n# List YAML\n",
        encoding="utf-8",
    )


@given("a temporary directory with a SKILL.md that has numeric description")
def step_temp_dir_numeric_desc(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "numeric-desc-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text(
        "---\nname: numeric-skill\ndescription: 42\n---\n\n# Numeric\n",
        encoding="utf-8",
    )


@given("a temporary directory with a SKILL.md that has empty description")
def step_temp_dir_empty_desc(context: Context) -> None:
    tmpdir = Path(tempfile.mkdtemp())
    context.tmpdir = tmpdir
    context.add_cleanup(lambda: shutil.rmtree(str(tmpdir), ignore_errors=True))
    folder = tmpdir / "empty-desc-skill"
    folder.mkdir()
    (folder / "SKILL.md").write_text(
        '---\nname: empty-desc\ndescription: ""\n---\n\n# Empty Desc\n',
        encoding="utf-8",
    )


@then('the first discovered skill description should contain "{text}"')
def step_first_skill_desc_contains(context: Context, text: str) -> None:
    assert len(context.discovered) > 0, "No discovered skills"
    desc = context.discovered[0].description
    assert text in desc, f"Expected '{text}' in description, got: {desc}"


@when("I call the noop handler")
def step_call_noop_handler(context: Context) -> None:
    from cleveragents.skills.discovery import _noop_handler

    context.noop_result = _noop_handler()


@then('the noop handler should return status "{expected}"')
def step_noop_result(context: Context, expected: str) -> None:
    assert context.noop_result["status"] == expected


# ---------------------------------------------------------------------------
# Source metadata
# ---------------------------------------------------------------------------


@given('a registered skill "{name}" with an agent_skill source')
def step_registered_skill_with_agent_source(context: Context, name: str) -> None:
    from cleveragents.application.services.skill_service import SkillService
    from cleveragents.skills.schema import (
        SkillAgentFolderSchema,
        SkillConfigSchema,
    )

    context.skill_service = SkillService()
    config = SkillConfigSchema(
        name=name,
        description="Test skill with agent_skill source",
        agent_skill_folders=[
            SkillAgentFolderSchema(path="/tmp/agent-skill-folder", name="test-agent")
        ],
    )
    context.skill_service.add_skill(config, config_path="/tmp/test.yaml")
    context.skill_name = name


@when('I resolve tools for "{name}"')
def step_resolve_tools(context: Context, name: str) -> None:
    skill, entries = context.skill_service.resolve_tools(name)
    context.resolved_skill = skill
    context.resolved_entries = entries


@then("the resolved tools should include agent_skill source entries")
def step_resolved_tools_agent_source(context: Context) -> None:
    agent_entries = [e for e in context.resolved_entries if "agent_skill:" in e.name]
    assert len(agent_entries) > 0, "Expected at least one agent_skill source entry"
