"""Step definitions for plan_service_coverage_boost.feature.

Targets uncovered lines in plan_service.py to increase branch and line coverage:
  - _build_actor_context: initial_context from config_blob dict
  - _build_actor_context: graph_descriptor from actor dict
  - _build_actor_context: context_variables fallback
  - _build_actor_context: graph_descriptor from config_blob fallback
  - _resolve_ai_provider_for_actor: empty provider raises PlanError
  - _strip_code_fences: markdown python and generic fences
  - _build_langsmith_config: returns {} when builder returns None
"""

from __future__ import annotations

import warnings
from datetime import datetime
from typing import Any
from unittest.mock import MagicMock

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

from cleveragents.application.services.plan_service import PlanService
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import PlanError
from cleveragents.domain.models.core import Actor

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


def _make_settings_mock() -> MagicMock:
    settings = MagicMock(spec=Settings)
    settings.is_langsmith_enabled = False
    settings.langsmith_tags = []
    settings.database_url = "sqlite://:memory:"
    settings.provider_configuration_diagnostics.return_value = {}
    settings.configured_provider_names.return_value = []
    settings.provider_expected_env_vars.return_value = []
    return settings


def _make_actor(
    name: str = "local/test-actor",
    provider: str = "mock",
    model: str = "mock-model",
    config_blob: dict[str, Any] | None = None,
    graph_descriptor: dict[str, Any] | None = None,
) -> Actor:
    return Actor(
        name=name,
        provider=provider,
        model=model,
        config_blob=config_blob or {},
        config_hash="abcd1234",
        graph_descriptor=graph_descriptor,
        created_at=datetime.now(),
        updated_at=datetime.now(),
    )


def _make_actor_no_provider(name: str = "local/no-provider") -> Actor:
    """Create an Actor with empty provider/model using model_construct."""
    return Actor.model_construct(
        name=name,
        provider="",
        model="",
        config_blob={},
        config_hash="abcd1234",
        created_at=datetime.now(),
        updated_at=datetime.now(),
        unsafe=False,
        is_default=False,
        is_built_in=False,
    )


# ---------------------------------------------------------------------------
# Background / Given
# ---------------------------------------------------------------------------


@given("I have a lightweight plan service for coverage boost testing")
def step_lightweight_plan_service(context: Context) -> None:
    settings = _make_settings_mock()
    uow = MagicMock()
    actor_service = MagicMock()
    with warnings.catch_warnings():
        warnings.simplefilter("ignore", DeprecationWarning)
        context.plan_svc = PlanService(
            settings=settings,
            unit_of_work=uow,
            actor_service=actor_service,
        )
    context.plan_svc_settings = settings
    context.plan_svc_actor_service = actor_service
    context.plan_svc_actor = None
    context.plan_svc_actor_ctx = None
    context.plan_svc_error = None
    context.plan_svc_stripped = None
    context.plan_svc_langsmith_result = None


@given("I have an actor with initial_context dict in config_blob")
def step_actor_with_initial_context(context: Context) -> None:
    context.plan_svc_actor = _make_actor(
        config_blob={"initial_context": {"key1": "val1", "key2": "val2"}},
    )


@given("I have an actor with a dict graph_descriptor")
def step_actor_with_graph_descriptor(context: Context) -> None:
    context.plan_svc_actor = _make_actor(
        graph_descriptor={"type": "langgraph", "entry": "main"},
    )


@given("the actor lookup returns an actor with empty provider")
def step_actor_with_empty_provider(context: Context) -> None:
    actor = _make_actor_no_provider()
    context.plan_svc_actor_service.get_actor.return_value = actor
    context.plan_svc_actor = actor


@given("LangSmith is enabled but build_langsmith_config returns None")
def step_langsmith_enabled_returns_none(context: Context) -> None:
    context.plan_svc_settings.is_langsmith_enabled = True
    context.plan_svc_settings.build_langsmith_config.return_value = None


@given("I have an actor with context_variables fallback in config_blob")
def step_actor_with_context_variables(context: Context) -> None:
    context.plan_svc_actor = _make_actor(
        config_blob={"context_variables": {"env": "production", "debug": "true"}},
    )


@given("I have an actor with graph_descriptor in config_blob but not on the actor")
def step_actor_graph_descriptor_in_config_blob(context: Context) -> None:
    context.plan_svc_actor = _make_actor(
        config_blob={"graph_descriptor": {"type": "custom", "nodes": []}},
        graph_descriptor=None,
    )


# ---------------------------------------------------------------------------
# When
# ---------------------------------------------------------------------------


@when("I build the actor context for the actor with initial_context")
def step_build_actor_context_initial(context: Context) -> None:
    context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
        context.plan_svc_actor
    )


@when("I build the actor context for the actor with graph_descriptor")
def step_build_actor_context_graph(context: Context) -> None:
    context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
        context.plan_svc_actor
    )


@when("I try to resolve the AI provider for the providerless actor")
def step_resolve_provider_empty(context: Context) -> None:
    try:
        context.plan_svc._resolve_ai_provider_for_actor("local/no-provider")
        context.plan_svc_error = None
    except PlanError as exc:
        context.plan_svc_error = exc


@when("I strip code fences from python-fenced content")
def step_strip_python_fences(context: Context) -> None:
    content = '```python\nprint("hello")\n```'
    context.plan_svc_stripped = context.plan_svc._strip_code_fences(content)


@when("I strip code fences from generic-fenced content")
def step_strip_generic_fences(context: Context) -> None:
    content = "```\nsome text\n```"
    context.plan_svc_stripped = context.plan_svc._strip_code_fences(content)


@when("I build the LangSmith config for a project")
def step_build_langsmith_config(context: Context) -> None:
    project = MagicMock()
    project.id = 1
    project.name = "test-project"
    context.plan_svc_langsmith_result = context.plan_svc._build_langsmith_config(
        project, None, run_name="test"
    )


@when("I build the actor context for the actor with context_variables")
def step_build_actor_context_context_variables(context: Context) -> None:
    context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
        context.plan_svc_actor
    )


@when("I build the actor context for the config_blob graph_descriptor actor")
def step_build_actor_context_config_blob_graph(context: Context) -> None:
    context.plan_svc_actor_ctx = context.plan_svc._build_actor_context(
        context.plan_svc_actor
    )


# ---------------------------------------------------------------------------
# Then
# ---------------------------------------------------------------------------


@then("the actor invocation context should contain the initial_context entries")
def step_assert_initial_context(context: Context) -> None:
    ctx = context.plan_svc_actor_ctx
    assert ctx is not None, "No actor context was built"
    assert ctx.initial_context == {"key1": "val1", "key2": "val2"}, (
        f"Expected initial_context with key1/key2, got {ctx.initial_context}"
    )


@then("the actor invocation context should contain the graph_descriptor")
def step_assert_graph_descriptor(context: Context) -> None:
    ctx = context.plan_svc_actor_ctx
    assert ctx is not None, "No actor context was built"
    assert ctx.graph_descriptor == {"type": "langgraph", "entry": "main"}, (
        f"Expected graph_descriptor, got {ctx.graph_descriptor}"
    )


@then(
    "the providerless actor PlanError should contain "
    '"Actor is missing provider configuration"'
)
def step_assert_provider_error(context: Context) -> None:
    err = context.plan_svc_error
    assert err is not None, "Expected PlanError but no error was raised"
    assert isinstance(err, PlanError), f"Expected PlanError, got {type(err).__name__}"
    assert "Actor is missing provider configuration" in err.message, (
        f"Expected 'Actor is missing provider configuration' in '{err.message}'"
    )


@then("the stripped content should contain only the inner code")
def step_assert_stripped_python(context: Context) -> None:
    assert context.plan_svc_stripped == 'print("hello")', (
        f"Expected inner code, got {context.plan_svc_stripped!r}"
    )


@then("the stripped content should contain only the inner text")
def step_assert_stripped_generic(context: Context) -> None:
    assert context.plan_svc_stripped == "some text", (
        f"Expected inner text, got {context.plan_svc_stripped!r}"
    )


@then("the plan service LangSmith config result should be an empty dict")
def step_assert_langsmith_empty(context: Context) -> None:
    result = context.plan_svc_langsmith_result
    assert result == {}, f"Expected empty dict, got {result}"


@then("the actor invocation context should contain the context_variables entries")
def step_assert_context_variables(context: Context) -> None:
    ctx = context.plan_svc_actor_ctx
    assert ctx is not None, "No actor context was built"
    assert ctx.initial_context == {"env": "production", "debug": "true"}, (
        f"Expected context_variables as initial_context, got {ctx.initial_context}"
    )


@then("the actor invocation context should contain the config_blob graph_descriptor")
def step_assert_config_blob_graph_descriptor(context: Context) -> None:
    ctx = context.plan_svc_actor_ctx
    assert ctx is not None, "No actor context was built"
    assert ctx.graph_descriptor == {"type": "custom", "nodes": []}, (
        f"Expected config_blob graph_descriptor, got {ctx.graph_descriptor}"
    )
