"""Step definitions for Agent Skills Loader Behave feature tests.

Covers AgentSkillSpec parsing, folder discovery, tool descriptor mapping,
progressive disclosure, namespaced naming, step ordering, and path safety.
"""

from __future__ import annotations

import textwrap
from pathlib import Path

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

from cleveragents.skills.agent_skills_loader import AgentSkillLoader, AgentSkillSpec

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


def _write_skill_md(folder: Path, content: str) -> None:
    """Write dedented content to SKILL.md in *folder*."""
    folder.mkdir(parents=True, exist_ok=True)
    (folder / "SKILL.md").write_text(textwrap.dedent(content).lstrip())


# ---------------------------------------------------------------------------
# Given — SKILL.md setup
# ---------------------------------------------------------------------------


@given("a SKILL.md file with frontmatter:")
def step_skill_md_with_frontmatter(context: Context) -> None:
    """Create a temporary SKILL.md with the given content."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    _write_skill_md(tmp_dir, context.text)
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("an empty SKILL.md file")
def step_empty_skill_md(context: Context) -> None:
    """Create an empty SKILL.md file."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_empty_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    (tmp_dir / "SKILL.md").write_text("")
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("an agent skills folder with a valid SKILL.md:")
def step_agent_skills_folder_with_skill_md(context: Context) -> None:
    """Create a temporary agent skills folder with a valid SKILL.md."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_folder_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    _write_skill_md(tmp_dir, context.text)
    context.skill_load_error = None
    context.loaded_skill = None
    context.tool_descriptor = None
    context.discover_result = None
    context.activate_result = None


@given("an agent skills folder without a SKILL.md file")
def step_agent_skills_folder_no_skill_md(context: Context) -> None:
    """Create a folder without SKILL.md."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_no_md_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    context.skill_load_error = None
    context.loaded_skill = None


@given("a non-existent agent skills folder path")
def step_nonexistent_agent_skills_folder(context: Context) -> None:
    """Set a path that does not exist."""
    context.skill_folder = Path("/tmp/__nonexistent_agent_skill_path_xyz__")
    context.skill_load_error = None
    context.loaded_skill = None


@given('the folder contains a script file "{filename}" with content "{content}"')
def step_folder_has_script_file(context: Context, filename: str, content: str) -> None:
    """Add a file to the scripts/ subfolder."""
    scripts_dir: Path = context.skill_folder / "scripts"
    scripts_dir.mkdir(exist_ok=True)
    (scripts_dir / filename).write_text(content)


@given('the folder contains a reference file "{filename}" with content "{content}"')
def step_folder_has_reference_file(
    context: Context, filename: str, content: str
) -> None:
    """Add a file to the references/ subfolder."""
    refs_dir: Path = context.skill_folder / "references"
    refs_dir.mkdir(exist_ok=True)
    (refs_dir / filename).write_text(content)


@given('the folder contains an asset file "{filename}" with content "{content}"')
def step_folder_has_asset_file(context: Context, filename: str, content: str) -> None:
    """Add a file to the assets/ subfolder."""
    assets_dir: Path = context.skill_folder / "assets"
    assets_dir.mkdir(exist_ok=True)
    (assets_dir / filename).write_text(content)


# ---------------------------------------------------------------------------
# When — Parsing and loading actions
# ---------------------------------------------------------------------------


@when("I parse the SKILL.md file")
def step_parse_skill_md(context: Context) -> None:
    """Parse the SKILL.md file using AgentSkillSpec."""
    skill_md_path = context.skill_folder / "SKILL.md"
    context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path)
    context.skill_parse_error = None


@when("I parse the SKILL.md file expecting an error")
def step_parse_skill_md_expecting_error(context: Context) -> None:
    """Attempt to parse the SKILL.md file, expecting a ValueError."""
    skill_md_path = context.skill_folder / "SKILL.md"
    try:
        context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path)
        context.skill_parse_error = None
    except (ValueError, FileNotFoundError) as exc:
        context.skill_parse_error = exc
        context.skill_parse_result = None


@when("I load the agent skills folder")
def step_load_agent_skills_folder(context: Context) -> None:
    """Load an agent skills folder using AgentSkillLoader."""
    context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder)
    context.skill_load_error = None


@when("I load the agent skills folder expecting an error")
def step_load_agent_skills_folder_expecting_error(context: Context) -> None:
    """Attempt to load an agent skills folder, expecting a ValueError."""
    try:
        context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder)
        context.skill_load_error = None
    except (ValueError, FileNotFoundError) as exc:
        context.skill_load_error = exc
        context.loaded_skill = None


@when("I convert the loaded agent skill to a tool descriptor")
def step_convert_to_tool_descriptor(context: Context) -> None:
    """Convert the loaded AgentSkillLoader to a ToolDescriptor."""
    context.tool_descriptor = context.loaded_skill.to_tool_descriptor()


@when("I call discover on the agent skill")
def step_call_discover(context: Context) -> None:
    """Call discover() which returns metadata-only descriptor."""
    context.loaded_skill = AgentSkillLoader.from_folder(context.skill_folder)
    context.discover_result = context.loaded_skill.discover()


@when("I call activate on the agent skill")
def step_call_activate(context: Context) -> None:
    """Call activate() to load the full SKILL.md body."""
    context.activate_result = context.loaded_skill.activate()


@when("I call deactivate on the agent skill")
def step_call_deactivate(context: Context) -> None:
    """Call deactivate() to clear the loaded body."""
    context.loaded_skill.deactivate()


@when("I list available resources")
def step_list_available_resources(context: Context) -> None:
    """List available resource files in the skill folder."""
    context.available_resources = context.loaded_skill.list_resources()


# ---------------------------------------------------------------------------
# Then — Assertions for parsing
# ---------------------------------------------------------------------------


@then('the parsed skill name should be "{expected}"')
def step_parsed_skill_name(context: Context, expected: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert result.name == expected, f"Expected name '{expected}', got '{result.name}'"


@then('the parsed skill description should be "{expected}"')
def step_parsed_skill_description(context: Context, expected: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert result.description == expected, (
        f"Expected description '{expected}', got '{result.description}'"
    )


@then('the parsed skill body should contain "{expected}"')
def step_parsed_skill_body_contains(context: Context, expected: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert expected in result.body, (
        f"Expected body to contain '{expected}', got:\n{result.body}"
    )


@then('the parsed skill version should be "{expected}"')
def step_parsed_skill_version(context: Context, expected: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert result.version == expected, (
        f"Expected version '{expected}', got '{result.version}'"
    )


@then('the parsed skill allowed tools should include "{tool_name}"')
def step_parsed_skill_allowed_tools_include(context: Context, tool_name: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert tool_name in result.allowed_tools, (
        f"Expected allowed_tools to include '{tool_name}', got: {result.allowed_tools}"
    )


@then('the parsed skill metadata should have key "{key}" with value "{value}"')
def step_parsed_skill_metadata_key(context: Context, key: str, value: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert result.metadata is not None, "No metadata on parsed skill"
    actual = str(result.metadata.get(key))
    assert actual == value, f"Expected metadata['{key}'] = '{value}', got '{actual}'"


@then('the agent skill parse error should mention "{keyword}"')
def step_agent_skill_parse_error_mentions(context: Context, keyword: str) -> None:
    err = context.skill_parse_error
    assert err is not None, "Expected a parse error but none was raised"
    assert keyword.lower() in str(err).lower(), (
        f"Expected error to mention '{keyword}', got: {err}"
    )


# ---------------------------------------------------------------------------
# Then — Assertions for folder loading
# ---------------------------------------------------------------------------


@then('the loaded agent skill name should be "{expected}"')
def step_loaded_skill_name(context: Context, expected: str) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.spec.name == expected, (
        f"Expected skill name '{expected}', got '{skill.spec.name}'"
    )


@then('the loaded agent skill description should be "{expected}"')
def step_loaded_skill_description(context: Context, expected: str) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.spec.description == expected, (
        f"Expected description '{expected}', got '{skill.spec.description}'"
    )


@then('the loaded agent skill script paths should include "{filename}"')
def step_loaded_skill_script_paths_include(context: Context, filename: str) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    names = [p.name for p in skill.script_paths]
    assert filename in names, (
        f"Expected script paths to include '{filename}', got: {names}"
    )


@then('the loaded agent skill reference paths should include "{filename}"')
def step_loaded_skill_reference_paths_include(context: Context, filename: str) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    names = [p.name for p in skill.reference_paths]
    assert filename in names, (
        f"Expected reference paths to include '{filename}', got: {names}"
    )


@then('the loaded agent skill asset paths should include "{filename}"')
def step_loaded_skill_asset_paths_include(context: Context, filename: str) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    names = [p.name for p in skill.asset_paths]
    assert filename in names, (
        f"Expected asset paths to include '{filename}', got: {names}"
    )


@then("the loaded agent skill script paths should be empty")
def step_loaded_skill_script_paths_empty(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.script_paths == [], (
        f"Expected empty script paths, got: {skill.script_paths}"
    )


@then("the loaded agent skill reference paths should be empty")
def step_loaded_skill_reference_paths_empty(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.reference_paths == [], (
        f"Expected empty reference paths, got: {skill.reference_paths}"
    )


@then("the loaded agent skill asset paths should be empty")
def step_loaded_skill_asset_paths_empty(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.asset_paths == [], (
        f"Expected empty asset paths, got: {skill.asset_paths}"
    )


@then('the agent skill load error should mention "{keyword}"')
def step_agent_skill_load_error_mentions(context: Context, keyword: str) -> None:
    err = context.skill_load_error
    assert err is not None, "Expected a load error but none was raised"
    assert keyword.lower() in str(err).lower(), (
        f"Expected error to mention '{keyword}', got: {err}"
    )


# ---------------------------------------------------------------------------
# Then — Assertions for tool descriptor mapping
# ---------------------------------------------------------------------------


@then('the tool descriptor source should be "{expected}"')
def step_tool_descriptor_source(context: Context, expected: str) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert td.source == expected, f"Expected source '{expected}', got '{td.source}'"


@then('the tool descriptor name should be "{expected}"')
def step_tool_descriptor_name(context: Context, expected: str) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert td.name == expected, (
        f"Expected tool descriptor name '{expected}', got '{td.name}'"
    )


@then("the tool descriptor should have read_only set to True")
def step_tool_descriptor_read_only_true(context: Context) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert td.read_only is True, f"Expected read_only=True, got {td.read_only}"


@then("the tool descriptor should have writes set to False")
def step_tool_descriptor_writes_false(context: Context) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert td.writes is False, f"Expected writes=False, got {td.writes}"


@then('the tool descriptor allowed tools should include "{tool_name}"')
def step_tool_descriptor_allowed_tools_include(
    context: Context, tool_name: str
) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert tool_name in td.allowed_tools, (
        f"Expected allowed_tools to include '{tool_name}', got: {td.allowed_tools}"
    )


@then("the tool descriptor agent_skill_path should be set")
def step_tool_descriptor_agent_skill_path_set(context: Context) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    assert td.agent_skill_path is not None, "Expected agent_skill_path to be set"
    assert td.agent_skill_path != "", "Expected agent_skill_path to be non-empty"


# ---------------------------------------------------------------------------
# Then — Assertions for progressive disclosure
# ---------------------------------------------------------------------------


@then('the discover result should have name "{expected}"')
def step_discover_result_name(context: Context, expected: str) -> None:
    dr = context.discover_result
    assert dr is not None, "No discover result available"
    assert dr.name == expected, f"Expected discover name '{expected}', got '{dr.name}'"


@then('the discover result should have description "{expected}"')
def step_discover_result_description(context: Context, expected: str) -> None:
    dr = context.discover_result
    assert dr is not None, "No discover result available"
    assert dr.description == expected, (
        f"Expected discover description '{expected}', got '{dr.description}'"
    )


@then("the discover result body should be empty")
def step_discover_result_body_empty(context: Context) -> None:
    dr = context.discover_result
    assert dr is not None, "No discover result available"
    assert dr.body == "" or dr.body is None, (
        f"Expected empty body on discover, got: '{dr.body}'"
    )


@then('the activate result body should contain "{expected}"')
def step_activate_result_body_contains(context: Context, expected: str) -> None:
    ar = context.activate_result
    assert ar is not None, "No activate result available"
    assert expected in ar.body, (
        f"Expected activate body to contain '{expected}', got:\n{ar.body}"
    )


@then('the resources list should include "{filename}"')
def step_resources_list_includes(context: Context, filename: str) -> None:
    resources = context.available_resources
    assert resources is not None, "No resources list available"
    names = [p.name for p in resources]
    assert filename in names, (
        f"Expected resources to include '{filename}', got: {names}"
    )


@then("the agent skill body should be cleared")
def step_agent_skill_body_cleared(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.active_body is None or skill.active_body == "", (
        f"Expected body to be cleared after deactivate, got: '{skill.active_body}'"
    )


# ---------------------------------------------------------------------------
# Then — Assertions for ordering
# ---------------------------------------------------------------------------


@then(
    "the parsed skill body steps should appear in order: "
    '"{first}", "{second}", "{third}"'
)
def step_parsed_skill_body_order(
    context: Context, first: str, second: str, third: str
) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    body = result.body
    pos_first = body.find(first)
    pos_second = body.find(second)
    pos_third = body.find(third)
    assert pos_first != -1, f"'{first}' not found in body"
    assert pos_second != -1, f"'{second}' not found in body"
    assert pos_third != -1, f"'{third}' not found in body"
    assert pos_first < pos_second < pos_third, (
        f"Expected order {first!r} < {second!r} < {third!r} in body, "
        f"got positions {pos_first}, {pos_second}, {pos_third}"
    )


# ---------------------------------------------------------------------------
# Then — Assertions for path safety
# ---------------------------------------------------------------------------


@then("all script paths should be relative to the skill folder")
def step_all_script_paths_relative_to_folder(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    folder = context.skill_folder
    for script_path in skill.script_paths:
        resolved = script_path.resolve()
        folder_resolved = folder.resolve()
        assert str(resolved).startswith(str(folder_resolved)), (
            f"Script path '{resolved}' is outside skill folder '{folder_resolved}'"
        )


# ---------------------------------------------------------------------------
# Given — Edge-case setup for coverage
# ---------------------------------------------------------------------------


@given("a non-existent SKILL.md path")
def step_nonexistent_skill_md_path(context: Context) -> None:
    """Set a path to a SKILL.md that does not exist."""
    context.skill_folder = Path("/tmp/__nonexistent_skill_folder_xyz__")
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("a SKILL.md with an empty frontmatter block")
def step_skill_md_empty_frontmatter_block(context: Context) -> None:
    """Create a SKILL.md with empty frontmatter (--- followed immediately by ---)."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_empty_fm_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    (tmp_dir / "SKILL.md").write_text("---\n---\nsome body\n")
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("a SKILL.md with malformed YAML frontmatter")
def step_skill_md_malformed_yaml(context: Context) -> None:
    """Create a SKILL.md with YAML that cannot be parsed."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_bad_yaml_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    (tmp_dir / "SKILL.md").write_text("---\nname: [\n---\nbody\n")
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("a SKILL.md with a YAML list as frontmatter instead of a mapping")
def step_skill_md_yaml_list_frontmatter(context: Context) -> None:
    """Create a SKILL.md with a YAML list (not a mapping) as frontmatter."""
    import tempfile

    tmp_dir = Path(tempfile.mkdtemp(prefix="agent_skill_list_fm_"))
    context._cleanup_handlers.append(
        lambda: __import__("shutil").rmtree(tmp_dir, ignore_errors=True)
    )
    context.skill_folder = tmp_dir
    (tmp_dir / "SKILL.md").write_text("---\n- item1\n- item2\n---\nbody\n")
    context.skill_parse_error = None
    context.skill_parse_result = None


@given("an agent skills folder path that is a file")
def step_agent_skills_folder_is_a_file(context: Context) -> None:
    """Create a temporary file and use its path as if it were a folder."""
    import os
    import tempfile

    fd, tmp_path = tempfile.mkstemp(prefix="agent_skill_file_", suffix=".md")
    os.close(fd)
    context._cleanup_handlers.append(lambda: Path(tmp_path).unlink(missing_ok=True))
    context.skill_folder = Path(tmp_path)
    context.skill_load_error = None
    context.loaded_skill = None


@given("the scripts folder contains a nested sub-directory")
def step_scripts_folder_has_nested_subdir(context: Context) -> None:
    """Add a nested sub-directory inside scripts/ (should be skipped)."""
    scripts_dir: Path = context.skill_folder / "scripts"
    scripts_dir.mkdir(exist_ok=True)
    nested = scripts_dir / "nested_subdir"
    nested.mkdir(exist_ok=True)


# ---------------------------------------------------------------------------
# When — Edge-case parse actions
# ---------------------------------------------------------------------------


@when("I parse the non-existent SKILL.md file expecting an error")
def step_parse_nonexistent_skill_md(context: Context) -> None:
    """Attempt to parse a SKILL.md at a path that doesn't exist."""
    skill_md_path = context.skill_folder / "SKILL.md"
    try:
        context.skill_parse_result = AgentSkillSpec.from_file(skill_md_path)
        context.skill_parse_error = None
    except (ValueError, FileNotFoundError) as exc:
        context.skill_parse_error = exc
        context.skill_parse_result = None


@when("I construct an AgentSkillLoader with None spec expecting an error")
def step_construct_loader_none_spec(context: Context) -> None:
    """Attempt to construct AgentSkillLoader with spec=None."""
    try:
        AgentSkillLoader(  # type: ignore[call-arg]
            spec=None,
            folder=Path("/tmp"),
            script_paths=[],
            reference_paths=[],
            asset_paths=[],
        )
        context.skill_load_error = None
    except (ValueError, TypeError) as exc:
        context.skill_load_error = exc


@when("I construct an AgentSkillLoader with None folder expecting an error")
def step_construct_loader_none_folder(context: Context) -> None:
    """Attempt to construct AgentSkillLoader with folder=None."""
    spec = AgentSkillSpec.from_string(
        "---\nname: local/test\ndescription: test\n---\nbody\n"
    )
    try:
        AgentSkillLoader(  # type: ignore[call-arg]
            spec=spec,
            folder=None,
            script_paths=[],
            reference_paths=[],
            asset_paths=[],
        )
        context.skill_load_error = None
    except (ValueError, TypeError, AttributeError) as exc:
        context.skill_load_error = exc


@when("I call from_folder with None expecting an error")
def step_call_from_folder_none(context: Context) -> None:
    """Call AgentSkillLoader.from_folder(None) and capture the error."""
    try:
        AgentSkillLoader.from_folder(None)  # type: ignore[arg-type]
        context.skill_load_error = None
    except (ValueError, TypeError, AttributeError) as exc:
        context.skill_load_error = exc


# ---------------------------------------------------------------------------
# Then — Edge-case assertions
# ---------------------------------------------------------------------------


@then("the loaded agent skill folder property should be set")
def step_loaded_skill_folder_property(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.folder is not None, "Expected folder property to be set"
    assert skill.folder.is_dir(), (
        f"Expected folder to be a directory, got: {skill.folder}"
    )


@then("the loaded agent skill script paths should not include any directories")
def step_loaded_skill_script_paths_no_dirs(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    for p in skill.script_paths:
        assert p.is_file(), (
            f"Expected only files in script_paths, but found directory: {p}"
        )


# ---------------------------------------------------------------------------
# Then — Assertions for structured steps
# ---------------------------------------------------------------------------


@then("the parsed skill steps should have {count:d} entries")
def step_parsed_skill_steps_count(context: Context, count: int) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    actual = len(result.steps)
    assert actual == count, f"Expected {count} step(s), got {actual}: {result.steps}"


@then('the parsed skill step {index:d} content should be "{expected}"')
def step_parsed_skill_step_content(context: Context, index: int, expected: str) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert len(result.steps) >= index, (
        f"Expected at least {index} step(s), got {len(result.steps)}"
    )
    actual = result.steps[index - 1].content
    assert actual == expected, (
        f"Expected step {index} content '{expected}', got '{actual}'"
    )


@then("the parsed skill step {index:d} index should be {expected:d}")
def step_parsed_skill_step_index(context: Context, index: int, expected: int) -> None:
    result = context.skill_parse_result
    assert result is not None, "No parsed skill result available"
    assert len(result.steps) >= index, (
        f"Expected at least {index} step(s), got {len(result.steps)}"
    )
    actual = result.steps[index - 1].index
    assert actual == expected, (
        f"Expected step {index} to have index={expected}, got {actual}"
    )


# ---------------------------------------------------------------------------
# Then — Assertions for resource binding slots
# ---------------------------------------------------------------------------


@then("the tool descriptor should have {count:d} resource slot(s)")
def step_tool_descriptor_resource_slot_count(context: Context, count: int) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    actual = len(td.resource_slots)
    assert actual == count, (
        f"Expected {count} resource slot(s), got {actual}: {td.resource_slots}"
    )


@then('the tool descriptor resource slot "{name}" should have access "{access}"')
def step_tool_descriptor_resource_slot_access(
    context: Context, name: str, access: str
) -> None:
    td = context.tool_descriptor
    assert td is not None, "No tool descriptor available"
    slot = next((s for s in td.resource_slots if s.name == name), None)
    assert slot is not None, (
        f"No resource slot named '{name}' found in: "
        f"{[s.name for s in td.resource_slots]}"
    )
    assert slot.access == access, (
        f"Expected slot '{name}' access='{access}', got '{slot.access}'"
    )


@then("the discover result should have {count:d} resource slot(s)")
def step_discover_result_resource_slot_count(context: Context, count: int) -> None:
    dr = context.discover_result
    assert dr is not None, "No discover result available"
    actual = len(dr.resource_slots)
    assert actual == count, (
        f"Expected {count} resource slot(s) in discover result, "
        f"got {actual}: {dr.resource_slots}"
    )


@then('the discover result resource slot "{name}" should have access "{access}"')
def step_discover_result_resource_slot_access(
    context: Context, name: str, access: str
) -> None:
    dr = context.discover_result
    assert dr is not None, "No discover result available"
    slot = next((s for s in dr.resource_slots if s.name == name), None)
    assert slot is not None, (
        f"No resource slot named '{name}' in discover result: "
        f"{[s.name for s in dr.resource_slots]}"
    )
    assert slot.access == access, (
        f"Expected discover slot '{name}' access='{access}', got '{slot.access}'"
    )


@then("the loaded skill resource_slots property should be empty")
def step_loaded_skill_resource_slots_property_empty(context: Context) -> None:
    skill = context.loaded_skill
    assert skill is not None, "No loaded skill available"
    assert skill.resource_slots == [], (
        f"Expected empty resource_slots property, got: {skill.resource_slots}"
    )
