"""Step definitions for plan_subplan_tool.feature.

Tests the builtin/plan-subplan tool: payload validation, defaults,
decision emission, rationale generation, and ToolRunner integration.
"""

from __future__ import annotations

import json
from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]
from behave.runner import Context  # type: ignore[import-untyped]
from ulid import ULID

from cleveragents.domain.models.core.decision import Decision
from cleveragents.tool.builtins.subplan_tool import (
    PLAN_SUBPLAN_SPEC,
    make_plan_subplan_spec,
)
from cleveragents.tool.registry import ToolRegistry
from cleveragents.tool.runner import ToolRunner

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

_PLAN_ID = str(ULID())
_SEQ = 0


# ---------------------------------------------------------------------------
# Stub decision service
# ---------------------------------------------------------------------------


class _StubDecisionService:
    """In-memory stub that records decisions without a database."""

    def __init__(self) -> None:
        self.recorded: list[Decision] = []

    def record_decision(self, decision: Decision) -> Decision:
        self.recorded.append(decision)
        return decision


# ---------------------------------------------------------------------------
# Helper: build minimal valid inputs
# ---------------------------------------------------------------------------


def _inputs(
    goal: str = "Implement feature X",
    resource_scopes: list[str] | None = None,
    project_ref: str | None = None,
    parallel: bool = False,
    merge_strategy: str | None = None,
    max_parallel: int | None = None,
    dependencies: list[str] | None = None,
    context_view: str | None = None,
) -> dict[str, Any]:
    payload: dict[str, Any] = {
        "goal": goal,
        "plan_id": _PLAN_ID,
        "sequence_number": _SEQ,
        "parallel": parallel,
    }
    if resource_scopes is not None:
        payload["resource_scopes"] = resource_scopes
    if project_ref is not None:
        payload["project_ref"] = project_ref
    if merge_strategy is not None:
        payload["merge_strategy"] = merge_strategy
    if max_parallel is not None:
        payload["max_parallel"] = max_parallel
    if dependencies is not None:
        payload["dependencies"] = dependencies
    if context_view is not None:
        payload["context_view"] = context_view
    return payload


# ---------------------------------------------------------------------------
# Given
# ---------------------------------------------------------------------------


@given("a plan_subplan tool handler")
def step_given_handler(ctx: Context) -> None:
    ctx.subplan_handler = PLAN_SUBPLAN_SPEC.handler


@given("a plan_subplan tool handler with a stub decision service")
def step_given_handler_with_service(ctx: Context) -> None:
    ctx.stub_service = _StubDecisionService()
    spec = make_plan_subplan_spec(decision_service=ctx.stub_service)
    ctx.subplan_handler = spec.handler


@given("a ToolRunner with the plan_subplan tool registered")
def step_given_runner(ctx: Context) -> None:
    registry = ToolRegistry()
    registry.register(PLAN_SUBPLAN_SPEC)
    ctx.runner = ToolRunner(registry)


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


@when('I invoke plan_subplan with goal "{goal}" and resource_scopes {scopes}')
def step_when_with_scopes(ctx: Context, goal: str, scopes: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(goal=goal, resource_scopes=json.loads(scopes))
    )


@when('I invoke plan_subplan with goal "{goal}" and project_ref "{ref}"')
def step_when_with_project_ref(ctx: Context, goal: str, ref: str) -> None:
    ctx.result = ctx.subplan_handler(_inputs(goal=goal, project_ref=ref))


@when("I invoke parallel plan_subplan with dependencies {deps}")
def step_when_parallel_with_deps(ctx: Context, deps: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(
            goal="Parallel work",
            resource_scopes=["src/"],
            parallel=True,
            dependencies=json.loads(deps),
        )
    )


@when('I invoke plan_subplan with goal "{goal}" and no resource scope or project ref')
def step_when_no_scope(ctx: Context, goal: str) -> None:
    ctx.result = ctx.subplan_handler(
        {"goal": goal, "plan_id": _PLAN_ID, "sequence_number": 0}
    )


@when("I invoke plan_subplan with empty goal and resource_scopes {scopes}")
def step_when_empty_goal(ctx: Context, scopes: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(goal="", resource_scopes=json.loads(scopes))
    )


@when("I invoke parallel plan_subplan with max_parallel {n:d}")
def step_when_bad_max_parallel(ctx: Context, n: int) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(resource_scopes=["src/"], parallel=True, max_parallel=n)
    )


@when('I invoke plan_subplan with invalid merge_strategy "{strategy}"')
def step_when_bad_strategy(ctx: Context, strategy: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(resource_scopes=["src/"], merge_strategy=strategy)
    )


@when('I invoke plan_subplan with invalid context_view "{view}"')
def step_when_bad_context_view(ctx: Context, view: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(resource_scopes=["src/"], context_view=view)
    )


@when("I invoke plan_subplan with a parent_decision_id and resource_scopes {scopes}")
def step_when_with_parent_decision(ctx: Context, scopes: str) -> None:
    parent_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
    inp = _inputs(resource_scopes=json.loads(scopes))
    inp["parent_decision_id"] = parent_id
    ctx.result = ctx.subplan_handler(inp)


@when('I invoke plan_subplan with invalid plan_id "{pid}" and resource_scopes {scopes}')
def step_when_invalid_plan_id(ctx: Context, pid: str, scopes: str) -> None:
    inp = _inputs(resource_scopes=json.loads(scopes))
    inp["plan_id"] = pid
    ctx.result = ctx.subplan_handler(inp)


@when(
    'I invoke plan_subplan with invalid dependency "{dep}" and resource_scopes {scopes}'
)
def step_when_invalid_dep(ctx: Context, dep: str, scopes: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(resource_scopes=json.loads(scopes), dependencies=[dep])
    )


@when(
    'I invoke plan_subplan with invalid parent_decision_id "{pid}" and resource_scopes {scopes}'
)
def step_when_invalid_parent_decision_id(ctx: Context, pid: str, scopes: str) -> None:
    inp = _inputs(resource_scopes=json.loads(scopes))
    inp["parent_decision_id"] = pid
    ctx.result = ctx.subplan_handler(inp)


@when("I invoke plan_subplan with empty resource scope item")
def step_when_empty_scope_item(ctx: Context) -> None:
    ctx.result = ctx.subplan_handler(_inputs(resource_scopes=[""]))


@when("I invoke parallel plan_subplan with no max_parallel override")
def step_when_default_max_parallel(ctx: Context) -> None:
    ctx.result = ctx.subplan_handler(_inputs(resource_scopes=["src/"], parallel=True))


@when('I invoke plan_subplan with context_view "{view}" and resource_scopes {scopes}')
def step_when_context_view(ctx: Context, view: str, scopes: str) -> None:
    ctx.result = ctx.subplan_handler(
        _inputs(resource_scopes=json.loads(scopes), context_view=view)
    )


@when("I register the plan_subplan tool in a ToolRegistry")
def step_when_register(ctx: Context) -> None:
    ctx.registry = ToolRegistry()
    ctx.registry.register(PLAN_SUBPLAN_SPEC)


@when("I run plan_subplan via the ToolRunner with valid inputs")
def step_when_run_via_runner(ctx: Context) -> None:
    ctx.runner_result = ctx.runner.execute(
        "builtin/plan-subplan",
        _inputs(goal="Runner test", resource_scopes=["src/"]),
    )


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


@then("the plan_subplan result should succeed")
def step_then_success(ctx: Context) -> None:
    r = ctx.result
    assert isinstance(r, dict), f"Expected dict, got {type(r)}"
    assert r.get("success") is True, f"Expected success=True, got: {r}"


@then("the plan_subplan result should fail")
def step_then_fail(ctx: Context) -> None:
    r = ctx.result
    assert isinstance(r, dict), f"Expected dict, got {type(r)}"
    assert r.get("success") is False, f"Expected success=False, got: {r}"


@then('the plan_subplan result decision_type should be "{dtype}"')
def step_then_dtype(ctx: Context, dtype: str) -> None:
    r = ctx.result
    assert r.get("decision_type") == dtype, (
        f"Expected decision_type={dtype!r}, got: {r.get('decision_type')!r}"
    )


@then('the plan_subplan error should mention "{keyword}"')
def step_then_error_keyword(ctx: Context, keyword: str) -> None:
    err = (r := ctx.result).get("error") or ""
    assert keyword.lower() in err.lower(), (
        f"Expected error to mention {keyword!r} in {r}"
    )


@then('the plan_subplan result output merge_strategy should be "{value}"')
def step_then_merge_strategy(ctx: Context, value: str) -> None:
    r = ctx.result
    assert r.get("merge_strategy") == value, (
        f"Expected merge_strategy={value!r}, got: {r.get('merge_strategy')!r}"
    )


@then("the plan_subplan result output max_parallel should be {n:d}")
def step_then_max_parallel(ctx: Context, n: int) -> None:
    r = ctx.result
    assert r.get("max_parallel") == n, (
        f"Expected max_parallel={n}, got: {r.get('max_parallel')!r}"
    )


@then("the plan_subplan result output parallel should be false")
def step_then_not_parallel(ctx: Context) -> None:
    r = ctx.result
    assert r.get("parallel") is False, (
        f"Expected parallel=False, got: {r.get('parallel')!r}"
    )


@then("the plan_subplan result output dependencies should be empty")
def step_then_empty_deps(ctx: Context) -> None:
    r = ctx.result
    assert r.get("dependencies") == [], f"Expected [], got: {r.get('dependencies')!r}"


@then("the plan_subplan result rationale should mention the goal")
def step_then_rationale_goal(ctx: Context) -> None:
    r = ctx.result
    rationale = r.get("rationale") or ""
    goal = r.get("goal") or ""
    assert goal and goal in rationale, (
        f"Expected rationale to contain goal {goal!r}, got: {rationale!r}"
    )


@then('the plan_subplan result rationale should mention "{keyword}"')
def step_then_rationale_keyword(ctx: Context, keyword: str) -> None:
    rationale = (ctx.result.get("rationale") or "").lower()
    assert keyword.lower() in rationale, (
        f"Expected rationale to mention {keyword!r}, got: {rationale!r}"
    )


@then('the plan_subplan result output context_view should be "{view}"')
def step_then_context_view(ctx: Context, view: str) -> None:
    r = ctx.result
    assert r.get("context_view") == view, (
        f"Expected context_view={view!r}, got: {r.get('context_view')!r}"
    )


@then('the plan_subplan result output parent_decision_id should be "{value}"')
def step_then_parent_decision_id(ctx: Context, value: str) -> None:
    r = ctx.result
    assert r.get("parent_decision_id") == value, (
        f"Expected parent_decision_id={value!r}, got: {r.get('parent_decision_id')!r}"
    )


@then("the stub decision service should have recorded {n:d} decision")
def step_then_service_recorded(ctx: Context, n: int) -> None:
    stub: _StubDecisionService = ctx.stub_service
    assert len(stub.recorded) == n, (
        f"Expected {n} recorded decisions, got {len(stub.recorded)}"
    )


@then('the recorded decision type should be "{dtype}"')
def step_then_recorded_dtype(ctx: Context, dtype: str) -> None:
    stub: _StubDecisionService = ctx.stub_service
    assert stub.recorded, "No decisions were recorded"
    assert str(stub.recorded[0].decision_type) == dtype, (
        f"Expected {dtype!r}, got {stub.recorded[0].decision_type!r}"
    )


@then('the registry should contain "{name}"')
def step_then_registry_contains(ctx: Context, name: str) -> None:
    spec = ctx.registry.get(name)
    assert spec is not None, f"Expected '{name}' in registry"


@then("the ToolRunner result should succeed")
def step_then_runner_success(ctx: Context) -> None:
    result = ctx.runner_result
    assert result.success, f"Expected success=True, error={result.error}"
