"""Behave steps for reactive application coverage boost - targeting uncovered lines.

Uncovered lines targeted:
  73-75:  _configure_logging handler creation when root logger has no handlers
  123-125: _ensure_agent_registered creates agent from config when not yet registered
  285-286: _initialize_graph_context generator in any() for partial stage_order
  291:     _initialize_graph_context resets writing_stage when value is invalid
  356:     _follow_chained_edges returns (msg, True) when next_node is "end"
  370:     _follow_chained_edges returns (msg, False) when next_node becomes falsy
"""

from __future__ import annotations

import logging
from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.reactive.application import ReactiveCleverAgentsApp
from cleveragents.reactive.config_parser import AgentConfig, ReactiveConfig
from cleveragents.reactive.route import RouteConfig, RouteType

# ---------------------------------------------------------------------------
# Scenario: Logging adds a handler when root logger has no handlers
# Targets lines 73-75
# ---------------------------------------------------------------------------


@given("a reactive app with all root logger handlers removed")
def step_app_with_no_handlers(context: Context) -> None:
    root_logger = logging.getLogger()
    context.original_handlers = list(root_logger.handlers)
    context.original_level = root_logger.level
    for handler in list(root_logger.handlers):
        root_logger.removeHandler(handler)
    context.app = ReactiveCleverAgentsApp()


@when("I configure logging with verbose level {level:d}")
def step_configure_logging_level(context: Context, level: int) -> None:
    context.app._configure_logging(level)  # pylint: disable=protected-access


@then("the root logger should have at least one handler")
def step_root_logger_has_handler(context: Context) -> None:
    root_logger = logging.getLogger()
    assert len(root_logger.handlers) >= 1, (
        "Expected root logger to have at least one handler"
    )


@then("the handler format should include the module name pattern")
def step_handler_format_contains_name(context: Context) -> None:
    root_logger = logging.getLogger()
    found = False
    for handler in root_logger.handlers:
        fmt = handler.formatter
        if fmt and "%(name)s" in fmt._fmt:
            found = True
            break
    assert found, "Expected at least one handler with '[%(name)s] %(message)s' format"
    # Restore original handlers to avoid side-effects on other tests
    for handler in list(root_logger.handlers):
        root_logger.removeHandler(handler)
    for handler in context.original_handlers:
        root_logger.addHandler(handler)
    root_logger.setLevel(context.original_level)


# ---------------------------------------------------------------------------
# Scenario: _ensure_agent_registered creates instance from config
# Targets lines 123-125
#
# The first loop in _register_agents_from_config iterates config.agents and
# registers each.  The second loop iterates routes and calls the nested
# _ensure_agent_registered for agents/operators.  To hit lines 123-125 we
# need an agent that is NOT in stream_router.agents during the route loop
# but IS found via config.agents.get().  We achieve this with a custom dict
# whose items() adds a new key only after its first iteration (the first
# loop), making it invisible to the first loop but present for the second.
# ---------------------------------------------------------------------------


class _LazyAgentsDict(dict):
    """Dict that injects a new agent key after its first items() call."""

    def __init__(self, initial: dict[str, Any], deferred_key: str, deferred_value: Any):
        super().__init__(initial)
        self._deferred_key = deferred_key
        self._deferred_value = deferred_value
        self._first_iteration_done = False

    def items(self):
        result = list(super().items())
        if not self._first_iteration_done:
            self._first_iteration_done = True
            self[self._deferred_key] = self._deferred_value
        return result


@given(
    "a reactive app with a route referencing an agent not yet registered but present in config"
)
def step_app_with_unregistered_config_agent(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()


@when("I register agents from config triggering deferred registration")
def step_register_triggering_deferred(context: Context) -> None:
    lazy_agents = _LazyAgentsDict(
        initial={
            "dummy_actor": AgentConfig(name="dummy_actor", type="custom", config={}),
        },
        deferred_key="target_actor",
        deferred_value=AgentConfig(name="target_actor", type="custom", config={}),
    )

    app = ReactiveCleverAgentsApp()
    route = RouteConfig(
        name="trigger_route",
        type=RouteType.STREAM,
        agents=["target_actor"],
        operators=[{"type": "map", "params": {"actor": "target_actor"}}],
    )
    app.config = ReactiveConfig(
        agents=lazy_agents,  # type: ignore[arg-type]
        routes={"trigger_route": route},
    )
    object.__setattr__(app.config, "agents", lazy_agents)

    app._register_agents_from_config()  # pylint: disable=protected-access

    context.test_app = app
    context.target_agent_name = "target_actor"


@then("the deferred config agent should be registered in the stream router")
def step_deferred_agent_registered(context: Context) -> None:
    assert context.target_agent_name in context.test_app.stream_router.agents, (
        f"Expected '{context.target_agent_name}' in stream_router.agents"
    )


@then("the deferred config agent alias should also be registered")
def step_deferred_agent_alias_registered(context: Context) -> None:
    alias = "target_agent"
    assert alias in context.test_app.stream_router.agents, (
        f"Expected alias '{alias}' in stream_router.agents"
    )


# ---------------------------------------------------------------------------
# Scenario: _initialize_graph_context refreshes stage_order when partial
# Targets lines 285-286
# ---------------------------------------------------------------------------


@given("a reactive app with a global context that has a partial stage order list")
def step_app_partial_stage_order(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.config = ReactiveConfig(
        global_context={
            "stage_order": ["intro", "discovery", "brainstorming"],
            "writing_stage": "intro",
            "paper_details": {"topic": "test"},
        }
    )


@when("I initialize the graph context")
def step_initialize_graph_context(context: Context) -> None:
    context.graph_context = context.app._initialize_graph_context()  # pylint: disable=protected-access


@then("the stage order should be refreshed to the full default list")
def step_stage_order_refreshed(context: Context) -> None:
    expected = [
        "intro",
        "discovery",
        "brainstorming",
        "vetting",
        "structure",
        "section_writing",
        "paper_review",
        "latex_generation",
    ]
    assert context.graph_context["stage_order"] == expected, (
        f"Expected default stage_order but got {context.graph_context['stage_order']}"
    )


# ---------------------------------------------------------------------------
# Scenario: _initialize_graph_context resets writing_stage when invalid
# Targets line 291
# ---------------------------------------------------------------------------


@given("a reactive app with a global context that has an invalid writing stage")
def step_app_invalid_writing_stage(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    full_stages = [
        "intro",
        "discovery",
        "brainstorming",
        "vetting",
        "structure",
        "section_writing",
        "paper_review",
        "latex_generation",
    ]
    context.app.config = ReactiveConfig(
        global_context={
            "stage_order": full_stages,
            "writing_stage": "nonexistent_stage",
            "paper_details": {"topic": "test"},
        }
    )


@then("the writing stage should be reset to intro")
def step_writing_stage_reset(context: Context) -> None:
    assert context.graph_context["writing_stage"] == "intro", (
        f"Expected writing_stage='intro' but got '{context.graph_context['writing_stage']}'"
    )


# ---------------------------------------------------------------------------
# Scenario: _follow_chained_edges returns when next_node is "end"
# Targets line 356
# ---------------------------------------------------------------------------


@given("a reactive app configured for chained edge traversal to end node")
def step_app_chained_to_end(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.config = ReactiveConfig()


@when("I follow chained edges starting from the end node")
def step_follow_chained_to_end(context: Context) -> None:
    result_msg, should_return = context.app._follow_chained_edges(  # pylint: disable=protected-access
        next_targets=["end"],
        current_message="final_output",
        context={},
        node_actor_map={},
        agents={},
        router_node=None,
        select_targets_fn=lambda _: [],
    )
    context.chained_message = result_msg
    context.chained_should_return = should_return


@then("the chained edge result should be the current message with should_return true")
def step_chained_end_result(context: Context) -> None:
    assert context.chained_message == "final_output", (
        f"Expected 'final_output' but got '{context.chained_message}'"
    )
    assert context.chained_should_return is True, "Expected should_return=True"


# ---------------------------------------------------------------------------
# Scenario: _follow_chained_edges falls through when next_node becomes falsy
# Targets line 370
# ---------------------------------------------------------------------------


@given("a reactive app configured for chained edge traversal to a falsy node")
def step_app_chained_to_falsy(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()
    context.app.config = ReactiveConfig()


@when("I follow chained edges starting from a node that chains to a falsy node")
def step_follow_chained_to_falsy(context: Context) -> None:
    call_count = {"n": 0}

    def select_targets(node: str) -> list[str]:
        call_count["n"] += 1
        if call_count["n"] == 1:
            return [""]  # empty string is falsy -> while loop exits
        return []

    result_msg, should_return = context.app._follow_chained_edges(  # pylint: disable=protected-access
        next_targets=["some_node"],
        current_message="test_message",
        context={},
        node_actor_map={},
        agents={},
        router_node=None,
        select_targets_fn=select_targets,
    )
    context.chained_message = result_msg
    context.chained_should_return = should_return


@then("the chained edge result should be the current message with should_return false")
def step_chained_falsy_result(context: Context) -> None:
    assert context.chained_message == "test_message", (
        f"Expected 'test_message' but got '{context.chained_message}'"
    )
    assert context.chained_should_return is False, "Expected should_return=False"


# ---------------------------------------------------------------------------
# Scenario: Reactive app resolves skill names and stores tools
# ---------------------------------------------------------------------------


@given("a reactive app with skill names resolved via a mock skill service")
def step_app_with_skill_names_resolved(context: Context) -> None:
    from cleveragents.domain.models.core.skill import ResolvedToolEntry

    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_entries = [
        ResolvedToolEntry(
            name="tool-a",
            source_skill="local/web-tools",
            is_inline=False,
        ),
        ResolvedToolEntry(
            name="tool-b",
            source_skill="local/web-tools",
            is_inline=True,
        ),
    ]
    mock_service.resolve_tools.return_value = (mock_skill, mock_entries)

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/web-tools"],
        )
    context.mock_service = mock_service


@when("I check the resolved skill tools")
def step_check_resolved_skill_tools(context: Context) -> None:
    context.resolved_tools = context.app.resolved_skill_tools
    context.skill_names_result = context.app.skill_names


@then("the app should have resolved skill tool entries")
def step_app_has_resolved_tools(context: Context) -> None:
    assert len(context.resolved_tools) == 2, (
        f"Expected 2 resolved tools but got {len(context.resolved_tools)}"
    )
    assert context.resolved_tools[0]["name"] == "tool-a"
    assert context.resolved_tools[0]["source_skill"] == "local/web-tools"
    assert context.resolved_tools[0]["is_inline"] is False
    assert context.resolved_tools[0]["operation"] == "identity"
    assert context.resolved_tools[0]["skill_source"] == "local/web-tools"
    assert context.resolved_tools[1]["name"] == "tool-b"
    assert context.resolved_tools[1]["is_inline"] is True
    assert context.resolved_tools[1]["skill_source"] == "local/web-tools"


@then("the skill names property should match the input")
def step_skill_names_match(context: Context) -> None:
    assert context.skill_names_result == ["local/web-tools"]


# ---------------------------------------------------------------------------
# Scenario: Reactive app raises error for unknown skill name
# ---------------------------------------------------------------------------


@given("a reactive app configured with an unknown skill name")
def step_app_with_unknown_skill(context: Context) -> None:
    mock_service = MagicMock()
    mock_service.resolve_tools.side_effect = KeyError(
        "Skill 'local/bad' is not registered"
    )
    context.mock_service = mock_service


@when("I attempt to create the app with the unknown skill")
def step_attempt_create_app_with_unknown_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    mock_container = MagicMock()
    mock_container.skill_service.return_value = context.mock_service

    context.raised_exception = None
    try:
        with patch(
            "cleveragents.reactive.application.get_container",
            return_value=mock_container,
        ):
            ReactiveCleverAgentsApp(skill_names=["local/bad"])
    except CleverAgentsException as exc:
        context.raised_exception = exc


@then("a CleverAgentsException should be raised with skill not found message")
def step_exception_raised_with_message(context: Context) -> None:
    assert context.raised_exception is not None, "Expected CleverAgentsException"
    assert "local/bad" in str(context.raised_exception)
    assert "not found" in str(context.raised_exception)


# ---------------------------------------------------------------------------
# Scenario: Reactive app merges skill tools into agent tool list
# ---------------------------------------------------------------------------


@given("a reactive app with skill tools resolved and a config with agents")
def step_app_with_skill_tools_and_config(context: Context) -> None:
    from cleveragents.domain.models.core.skill import ResolvedToolEntry

    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_entries = [
        ResolvedToolEntry(
            name="skill-tool-x",
            source_skill="local/test-skill",
            is_inline=False,
        ),
    ]
    mock_service.resolve_tools.return_value = (mock_skill, mock_entries)

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/test-skill"],
        )


@when("agents are registered from config with skill tools")
def step_register_agents_with_skill_tools(context: Context) -> None:
    agent_cfg = AgentConfig(
        name="test_actor",
        type="custom",
        config={"tools": [{"operation": "uppercase"}]},
    )
    context.app.config = ReactiveConfig(
        agents={"test_actor": agent_cfg},
        routes={},
    )
    context.app._register_agents_from_config()  # pylint: disable=protected-access


@then("the agents should have skill tools merged into their tool list")
def step_agents_have_merged_tools(context: Context) -> None:
    from cleveragents.reactive.stream_router import SimpleToolAgent

    agent = context.app.stream_router.agents.get("test_actor")
    assert agent is not None, "Expected test_actor to be registered"
    assert isinstance(agent, SimpleToolAgent), "Expected SimpleToolAgent instance"
    # Original tool + 1 skill tool = 2
    assert len(agent.tools) == 2, f"Expected 2 tools but got {len(agent.tools)}"
    assert agent.tools[0] == {"operation": "uppercase"}
    assert agent.tools[1]["name"] == "skill-tool-x"
    assert agent.tools[1]["skill_source"] == "local/test-skill"


# ---------------------------------------------------------------------------
# Scenario: Reactive app works without skill names
# ---------------------------------------------------------------------------


@given("a reactive app created without any skill names")
def step_app_without_skills(context: Context) -> None:
    context.app = ReactiveCleverAgentsApp()


@when("I check the skill related properties")
def step_check_skill_properties(context: Context) -> None:
    context.skill_names_result = context.app.skill_names
    context.resolved_tools_result = context.app.resolved_skill_tools


@then("the skill names should be empty")
def step_skill_names_empty(context: Context) -> None:
    assert context.skill_names_result == [], (
        f"Expected empty skill names but got {context.skill_names_result}"
    )


@then("the resolved skill tools should be empty")
def step_resolved_tools_empty(context: Context) -> None:
    assert context.resolved_tools_result == [], (
        f"Expected empty resolved tools but got {context.resolved_tools_result}"
    )


# ---------------------------------------------------------------------------
# Scenario: Reactive app resolves skill with tool overrides
# ---------------------------------------------------------------------------


@given("a reactive app with skill names resolved including overrides")
def step_app_with_overrides(context: Context) -> None:
    from cleveragents.domain.models.core.skill import ResolvedToolEntry

    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_entries = [
        ResolvedToolEntry(
            name="tool-with-overrides",
            source_skill="local/override-skill",
            is_inline=False,
            overrides={"timeout": 600, "retries": 3},
        ),
    ]
    mock_service.resolve_tools.return_value = (mock_skill, mock_entries)

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/override-skill"],
        )


@then("the resolved tool entry should include overrides")
def step_resolved_tool_has_overrides(context: Context) -> None:
    assert len(context.resolved_tools) == 1, (
        f"Expected 1 resolved tool but got {len(context.resolved_tools)}"
    )
    tool = context.resolved_tools[0]
    assert tool["name"] == "tool-with-overrides"
    assert tool["skill_source"] == "local/override-skill"
    assert "overrides" in tool, "Expected 'overrides' key in tool dict"
    assert tool["overrides"] == {"timeout": 600, "retries": 3}


# ---------------------------------------------------------------------------
# Scenario: Reactive app deduplicates skill names (M7)
# ---------------------------------------------------------------------------


@given("a reactive app with duplicate skill names resolved via a mock skill service")
def step_app_with_duplicate_skills(context: Context) -> None:
    from cleveragents.domain.models.core.skill import ResolvedToolEntry

    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_entries = [
        ResolvedToolEntry(
            name="tool-a",
            source_skill="local/web-tools",
            is_inline=False,
        ),
    ]
    mock_service.resolve_tools.return_value = (mock_skill, mock_entries)

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/web-tools", "local/web-tools", "local/web-tools"],
        )
    context.mock_service = mock_service


@then("the skill names should be deduplicated")
def step_skill_names_deduplicated(context: Context) -> None:
    assert context.app.skill_names == ["local/web-tools"], (
        f"Expected deduplicated list but got {context.app.skill_names}"
    )


@then("resolve_tools should be called once per unique skill")
def step_resolve_called_once(context: Context) -> None:
    context.mock_service.resolve_tools.assert_called_once_with("local/web-tools")


# ---------------------------------------------------------------------------
# Scenario: Reactive app raises error for empty string skill name (m3)
# ---------------------------------------------------------------------------


@given("a reactive app configured with an empty string skill name")
def step_app_with_empty_string_skill(context: Context) -> None:
    # No mock service needed — the empty string is rejected by
    # _sanitize_skill_name before _resolve_skills is even called.
    pass


@when("I attempt to create the app with the empty skill")
def step_attempt_create_app_with_empty_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    context.raised_exception = None
    try:
        ReactiveCleverAgentsApp(skill_names=[""])
    except CleverAgentsException as exc:
        context.raised_exception = exc


@then("a CleverAgentsException should be raised for resolution failure")
def step_exception_raised_for_resolution_failure(context: Context) -> None:
    assert context.raised_exception is not None, "Expected CleverAgentsException"
    msg = str(context.raised_exception)
    assert "Invalid skill name" in msg, f"Expected 'Invalid skill name' in: {msg}"


# ---------------------------------------------------------------------------
# Scenario: Reactive app skill injection skips LLM agents (C1)
# ---------------------------------------------------------------------------


@given("a reactive app with skill tools and a config with an LLM agent")
def step_app_with_skill_tools_and_llm_agent(context: Context) -> None:
    from cleveragents.domain.models.core.skill import ResolvedToolEntry

    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_entries = [
        ResolvedToolEntry(
            name="skill-tool-x",
            source_skill="local/test-skill",
            is_inline=False,
        ),
    ]
    mock_service.resolve_tools.return_value = (mock_skill, mock_entries)

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/test-skill"],
        )


@when("agents are registered from config with skill tools present")
def step_register_agents_with_skill_tools_and_llm(context: Context) -> None:
    llm_agent_cfg = AgentConfig(
        name="llm_actor",
        type="llm",
        config={"system_prompt": "You are helpful.", "model": "test"},
    )
    tool_agent_cfg = AgentConfig(
        name="tool_actor",
        type="custom",
        config={"tools": [{"operation": "uppercase"}]},
    )
    context.app.config = ReactiveConfig(
        agents={"llm_actor": llm_agent_cfg, "tool_actor": tool_agent_cfg},
        routes={},
    )
    context.app._register_agents_from_config()  # pylint: disable=protected-access


@then("the LLM agent should remain a SimpleLLMAgent not a SimpleToolAgent")
def step_llm_agent_not_converted(context: Context) -> None:
    from cleveragents.reactive.stream_router import SimpleLLMAgent, SimpleToolAgent

    llm_agent = context.app.stream_router.agents.get("llm_actor")
    assert llm_agent is not None, "Expected llm_actor to be registered"
    assert isinstance(llm_agent, SimpleLLMAgent), (
        f"Expected SimpleLLMAgent but got {type(llm_agent).__name__}"
    )

    tool_agent = context.app.stream_router.agents.get("tool_actor")
    assert tool_agent is not None, "Expected tool_actor to be registered"
    assert isinstance(tool_agent, SimpleToolAgent), (
        f"Expected SimpleToolAgent but got {type(tool_agent).__name__}"
    )
    # Tool agent should have original tool + skill tool
    assert len(tool_agent.tools) == 2, (
        f"Expected 2 tools but got {len(tool_agent.tools)}"
    )


# ---------------------------------------------------------------------------
# Scenario: Reactive app raises error for skill resolution ValueError
# Targets: except ValueError branch in _resolve_skills()
# ---------------------------------------------------------------------------


@given("a reactive app configured with a skill that triggers a ValueError")
def step_app_with_valueerror_skill(context: Context) -> None:
    mock_service = MagicMock()
    mock_service.resolve_tools.side_effect = ValueError(
        "Cycle detected in skill includes"
    )
    context.mock_service = mock_service


@when("I attempt to create the app with the ValueError skill")
def step_attempt_create_app_with_valueerror_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    mock_container = MagicMock()
    mock_container.skill_service.return_value = context.mock_service

    context.raised_exception = None
    try:
        with patch(
            "cleveragents.reactive.application.get_container",
            return_value=mock_container,
        ):
            ReactiveCleverAgentsApp(skill_names=["local/cycle-skill"])
    except CleverAgentsException as exc:
        context.raised_exception = exc


@then("a CleverAgentsException should be raised with resolution failed message")
def step_exception_raised_with_resolution_failed(context: Context) -> None:
    assert context.raised_exception is not None, "Expected CleverAgentsException"
    msg = str(context.raised_exception)
    assert "resolution failed" in msg, f"Expected 'resolution failed' in: {msg}"
    assert "local/cycle-skill" in msg, f"Expected 'local/cycle-skill' in: {msg}"


# ---------------------------------------------------------------------------
# Scenario: Reactive app handles skill that resolves to zero tools
# Targets: zero-tool warning path (lines 116-124)
# ---------------------------------------------------------------------------


@given("a reactive app with a skill that resolves to zero tools")
def step_app_with_zero_tool_skill(context: Context) -> None:
    mock_service = MagicMock()
    mock_skill = MagicMock()
    mock_service.resolve_tools.return_value = (mock_skill, [])

    mock_container = MagicMock()
    mock_container.skill_service.return_value = mock_service

    with patch(
        "cleveragents.reactive.application.get_container",
        return_value=mock_container,
    ):
        context.app = ReactiveCleverAgentsApp(
            skill_names=["local/empty-skill"],
        )


@when("I check the resolved skill tools after zero tool resolution")
def step_check_resolved_skill_tools_zero(context: Context) -> None:
    context.resolved_tools_result = context.app.resolved_skill_tools
    context.skill_names_result = context.app.skill_names


@then("the skill names should contain the zero-tool skill")
def step_skill_names_contain_zero_tool(context: Context) -> None:
    assert context.skill_names_result == ["local/empty-skill"], (
        f"Expected ['local/empty-skill'] but got {context.skill_names_result}"
    )


# ---------------------------------------------------------------------------
# Scenario: Reactive app rejects skill name exceeding max length
# Targets: _sanitize_skill_name edge case — name > 127+1+127 = 255 chars
# ---------------------------------------------------------------------------


@given("a reactive app configured with an overly long skill name")
def step_app_with_long_skill_name(context: Context) -> None:
    # 128 chars in namespace segment exceeds 127 limit
    context.long_name = "a" * 128 + "/" + "b" * 10


@when("I attempt to create the app with the long skill name")
def step_attempt_create_app_with_long_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    context.raised_exception = None
    try:
        ReactiveCleverAgentsApp(skill_names=[context.long_name])
    except CleverAgentsException as exc:
        context.raised_exception = exc


@then("a CleverAgentsException should be raised for invalid name format")
def step_exception_raised_for_invalid_name(context: Context) -> None:
    assert context.raised_exception is not None, "Expected CleverAgentsException"
    msg = str(context.raised_exception)
    assert "Invalid skill name" in msg, f"Expected 'Invalid skill name' in: {msg}"


# ---------------------------------------------------------------------------
# Scenario: Reactive app strips ANSI escape codes from skill names
# ---------------------------------------------------------------------------


@given("a reactive app configured with a skill name containing ANSI codes")
def step_app_with_ansi_skill_name(context: Context) -> None:
    # ANSI escape code \x1b[31m is a control character sequence
    context.ansi_name = "\x1b[31mlocal/bad\x1b[0m"


@when("I attempt to create the app with the ANSI skill name")
def step_attempt_create_app_with_ansi_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    context.raised_exception = None
    try:
        ReactiveCleverAgentsApp(skill_names=[context.ansi_name])
    except CleverAgentsException as exc:
        context.raised_exception = exc


# ---------------------------------------------------------------------------
# Scenario: Reactive app rejects skill name with disallowed characters
# ---------------------------------------------------------------------------


@given("a reactive app configured with a skill name containing special characters")
def step_app_with_special_char_skill_name(context: Context) -> None:
    context.special_name = "local/bad@skill!"


@when("I attempt to create the app with the special char skill name")
def step_attempt_create_app_with_special_char_skill(context: Context) -> None:
    from cleveragents.core.exceptions import CleverAgentsException

    context.raised_exception = None
    try:
        ReactiveCleverAgentsApp(skill_names=[context.special_name])
    except CleverAgentsException as exc:
        context.raised_exception = exc
