"""Step definitions for models_lifecycle_coverage_r2.feature.

Provides branch coverage for ``LifecycleActionModel`` and
``LifecyclePlanModel`` conversion helpers and plan helper functions in
``cleveragents.infrastructure.database.models``.

All step-text uses an ``r2mod-`` prefix to avoid collisions with
existing step definition files.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

from behave import given, then, when
from sqlalchemy.orm import attributes as sa_attr

from cleveragents.infrastructure.database.models import (
    ActionArgumentModel,
    ActionInvariantModel,
    LifecycleActionModel,
    LifecyclePlanModel,
    PlanArgumentModel,
)

# ===================================================================
# Helpers
# ===================================================================

_NOW_ISO: str = datetime.now(tz=UTC).isoformat()

# Valid ULID constant for use in test models
_ULID_PLAN = "01ARZ3NDEKTSV4RRFFQ69G5FA2"


def _set_rel_none(model: Any, attr: str) -> None:
    """Bypass SQLAlchemy's instrumented setter to force a relationship to None.

    This lets us test the ``or []`` fallback branches in ``to_domain()``.
    """
    state = sa_attr.instance_state(model)
    state.dict[attr] = None


def _make_action_model(
    *,
    arguments: list[ActionArgumentModel] | None = None,
    invariants: list[ActionInvariantModel] | None = None,
    tags_json: str | None = "[]",
    inputs_schema_json: str | None = None,
) -> LifecycleActionModel:
    """Create a minimal ``LifecycleActionModel``."""
    model = LifecycleActionModel(
        namespaced_name="local/test-action",
        namespace="local",
        name="test-action",
        description="Test action",
        long_description=None,
        definition_of_done="tests pass",
        strategy_actor="local/strat",
        execution_actor="local/exec",
        review_actor=None,
        apply_actor=None,
        estimation_actor=None,
        invariant_actor=None,
        automation_profile=None,
        reusable=True,
        read_only=False,
        inputs_schema_json=inputs_schema_json,
        state="available",
        created_by=None,
        tags_json=tags_json,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    if arguments is not None:
        model.arguments_rel = arguments
    else:
        model.arguments_rel = []
    if invariants is not None:
        model.invariants_rel = invariants
    else:
        model.invariants_rel = []
    return model


def _make_plan_model(
    *,
    automation_profile: str | None = None,
    sandbox_refs_json: str | None = None,
    validation_summary_json: str | None = None,
    tags_json: str | None = None,
    action_name: str | None = None,
    error_details_json: str | None = None,
    error_message: str | None = None,
    arguments: list[PlanArgumentModel] | None = None,
) -> LifecyclePlanModel:
    """Create a minimal ``LifecyclePlanModel``."""
    model = LifecyclePlanModel(
        plan_id=_ULID_PLAN,
        parent_plan_id=None,
        root_plan_id=_ULID_PLAN,
        action_name=action_name or "",
        namespaced_name="local/test-plan",
        namespace="local",
        phase="action",
        processing_state="queued",
        attempt=1,
        description="Test plan",
        definition_of_done=None,
        strategy_actor=None,
        execution_actor=None,
        review_actor=None,
        apply_actor=None,
        estimation_actor=None,
        invariant_actor=None,
        automation_profile=automation_profile or "balanced",
        effective_profile_snapshot="{}",
        reusable=True,
        read_only=False,
        inputs_schema_json=None,
        changeset_id=None,
        sandbox_refs_json=sandbox_refs_json,
        validation_summary_json=validation_summary_json,
        decision_root_id=None,
        error_message=error_message,
        error_details_json=error_details_json,
        created_by=None,
        tags_json=tags_json or "[]",
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
        completed_at=None,
        strategize_started_at=None,
        strategize_completed_at=None,
        execute_started_at=None,
        execute_completed_at=None,
        apply_started_at=None,
        applied_at=None,
    )
    model.project_links_rel = []
    model.invariants_rel = []
    if arguments is not None:
        model.arguments_rel = arguments
    else:
        model.arguments_rel = []
    return model


# ===================================================================
# LifecycleActionModel.to_domain()
# ===================================================================


@given("a r2mod-ActionModel with an argument having None defaults")
def step_action_arg_none_defaults(context: Any) -> None:
    arg = ActionArgumentModel(
        name="arg1",
        arg_type="string",
        requirement="required",
        description="desc",
        default_value_json=None,
        min_value=None,
        max_value=None,
        validation_pattern=None,
        position=0,
    )
    context.r2_action_model = _make_action_model(arguments=[arg])


@given("a r2mod-ActionModel with None tags_json and None inputs_schema_json")
def step_action_none_tags_inputs(context: Any) -> None:
    context.r2_action_model = _make_action_model(
        tags_json=None, inputs_schema_json=None
    )


@given("a r2mod-ActionModel with None rels")
def step_action_none_rels(context: Any) -> None:
    model = _make_action_model()
    _set_rel_none(model, "arguments_rel")
    _set_rel_none(model, "invariants_rel")
    context.r2_action_model = model


@when("I r2mod-convert the ActionModel to domain")
def step_action_to_domain(context: Any) -> None:
    context.r2_action_domain = context.r2_action_model.to_domain()


@then("the r2mod-action first argument default_value should be None")
def step_action_arg_default_none(context: Any) -> None:
    assert context.r2_action_domain.arguments[0].default_value is None


@then("the r2mod-action first argument min_value should be None")
def step_action_arg_min_none(context: Any) -> None:
    assert context.r2_action_domain.arguments[0].min_value is None


@then("the r2mod-action first argument max_value should be None")
def step_action_arg_max_none(context: Any) -> None:
    assert context.r2_action_domain.arguments[0].max_value is None


@then("the r2mod-action tags should be empty")
def step_action_tags_empty(context: Any) -> None:
    assert context.r2_action_domain.tags == []


@then("the r2mod-action inputs_schema should be None")
def step_action_inputs_none(context: Any) -> None:
    assert context.r2_action_domain.inputs_schema is None


@then("the r2mod-action arguments should be empty")
def step_action_args_empty(context: Any) -> None:
    assert context.r2_action_domain.arguments == []


@then("the r2mod-action invariants should be empty")
def step_action_invs_empty(context: Any) -> None:
    assert context.r2_action_domain.invariants == []


# ===================================================================
# LifecyclePlanModel.to_domain()
# ===================================================================


@given("a r2mod-PlanModel with all optional fields set to None")
def step_plan_all_none(context: Any) -> None:
    model = _make_plan_model(
        automation_profile=None,
        sandbox_refs_json=None,
        validation_summary_json=None,
        tags_json=None,
        action_name="local/test-action",  # required non-empty by Plan domain
        error_details_json=None,
    )
    _set_rel_none(model, "project_links_rel")
    _set_rel_none(model, "invariants_rel")
    _set_rel_none(model, "arguments_rel")
    context.r2_plan_model = model


@given("a r2mod-PlanModel with None action_name")
def step_plan_none_action(context: Any) -> None:
    context.r2_plan_model = _make_plan_model(action_name=None)


@when("I r2mod-attempt to convert the PlanModel to domain")
def step_plan_to_domain_attempt(context: Any) -> None:
    try:
        context.r2_plan_domain = context.r2_plan_model.to_domain()
        context.r2_error = None
    except Exception as exc:
        context.r2_error = exc


@then('a r2mod-ValidationError should have been raised with "{fragment}"')
def step_r2_validation_error_fragment(context: Any, fragment: str) -> None:
    assert context.r2_error is not None, "Expected an error but none was raised"
    assert fragment in str(context.r2_error), (
        f"Expected '{fragment}' in error: {context.r2_error}"
    )


@given("a r2mod-PlanModel with an argument having None value_json")
def step_plan_arg_none_value(context: Any) -> None:
    arg = PlanArgumentModel(
        name="test_arg",
        value_json=None,
        value_type="string",
        position=0,
    )
    context.r2_plan_model = _make_plan_model(
        arguments=[arg], action_name="local/test-action"
    )


@when("I r2mod-convert the PlanModel to domain")
def step_plan_to_domain(context: Any) -> None:
    context.r2_plan_domain = context.r2_plan_model.to_domain()


@then("the r2mod-plan automation_profile should be None")
def step_plan_profile_none(context: Any) -> None:
    assert context.r2_plan_domain.automation_profile is None


@then("the r2mod-plan validation_summary should be None")
def step_plan_validation_none(context: Any) -> None:
    assert context.r2_plan_domain.validation_summary is None


@then("the r2mod-plan sandbox_refs should be empty")
def step_plan_sandbox_empty(context: Any) -> None:
    assert context.r2_plan_domain.sandbox_refs == []


@then("the r2mod-plan tags should be empty")
def step_plan_tags_empty(context: Any) -> None:
    assert context.r2_plan_domain.tags == []


@then("the r2mod-plan action_name should be empty string")
def step_plan_action_empty(context: Any) -> None:
    assert context.r2_plan_domain.action_name == ""


@then("the r2mod-plan error_details should be None")
def step_plan_err_none(context: Any) -> None:
    assert context.r2_plan_domain.error_details is None


@then('the r2mod-plan argument "{name}" should be None')
def step_plan_arg_value_none(context: Any, name: str) -> None:
    assert context.r2_plan_domain.arguments[name] is None


# ===================================================================
# LifecycleActionModel.from_domain()
# ===================================================================


@when("I r2mod-create ActionModel from domain with None inputs_schema")
def step_action_from_domain_none_schema(context: Any) -> None:
    from cleveragents.domain.models.core.action import Action, ActionState
    from cleveragents.domain.models.core.plan import NamespacedName

    action = Action(
        namespaced_name=NamespacedName(namespace="local", name="test-act"),
        description="test",
        definition_of_done="tests pass",
        strategy_actor="local/strat",
        execution_actor="local/exec",
        inputs_schema=None,
        state=ActionState.AVAILABLE,
    )
    context.r2_created_action = LifecycleActionModel.from_domain(action)


@then("the r2mod-created ActionModel inputs_schema_json should be None")
def step_action_created_schema_none(context: Any) -> None:
    assert context.r2_created_action.inputs_schema_json is None


@when("I r2mod-create ActionModel from domain with argument having None default")
def step_action_from_domain_arg_none_default(context: Any) -> None:
    from cleveragents.domain.models.core.action import (
        Action,
        ActionArgument,
        ActionState,
        ArgumentRequirement,
        ArgumentType,
    )
    from cleveragents.domain.models.core.plan import NamespacedName

    arg = ActionArgument(
        name="myarg",
        arg_type=ArgumentType.STRING,
        requirement=ArgumentRequirement.OPTIONAL,
        description="no default",
        default_value=None,
    )
    action = Action(
        namespaced_name=NamespacedName(namespace="local", name="test-act"),
        description="test",
        definition_of_done="tests pass",
        strategy_actor="local/strat",
        execution_actor="local/exec",
        arguments=[arg],
        state=ActionState.AVAILABLE,
    )
    context.r2_created_action = LifecycleActionModel.from_domain(action)


@then("the r2mod-created ActionModel first argument default_value_json should be None")
def step_action_created_arg_default_none(context: Any) -> None:
    assert context.r2_created_action.arguments_rel[0].default_value_json is None


# ===================================================================
# LifecyclePlanModel helpers
# ===================================================================


@then("r2mod-_parse_iso with None should return None")
def step_parse_iso_none(context: Any) -> None:
    assert LifecyclePlanModel._parse_iso(None) is None


@then("r2mod-_to_iso with None should return None")
def step_to_iso_none(context: Any) -> None:
    assert LifecyclePlanModel._to_iso(None) is None


@then('r2mod-_parse_iso with "{iso}" should return a datetime')
def step_parse_iso_value(context: Any, iso: str) -> None:
    result = LifecyclePlanModel._parse_iso(iso)
    assert isinstance(result, datetime)


@then("r2mod-_to_iso with a datetime should return an ISO string")
def step_to_iso_value(context: Any) -> None:
    now = datetime.now(tz=UTC)
    result = LifecyclePlanModel._to_iso(now)
    assert isinstance(result, str)
    assert "T" in result
