"""Step definitions for models_record_coverage_r2.feature.

Provides branch coverage for ``SessionModel``, ``SessionMessageModel``,
``ToolModel``, and ``NamespacedProjectModel`` conversion helpers 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 types import SimpleNamespace
from typing import Any

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

from cleveragents.infrastructure.database.models import (
    NamespacedProjectModel,
    SessionMessageModel,
    SessionModel,
    ToolModel,
)

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

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

# Valid ULID constants for use in test models
_ULID_SESS: str = "01HTEST0000000000000SESS01"  # invalid; replaced below
_ULID_MSG1: str = "01HTEST0000000000000MSG001"

# Actually valid ULIDs matching ^[0-9A-HJKMNP-TV-Z]{26}$
_ULID_SESS = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
_ULID_MSG1 = "01ARZ3NDEKTSV4RRFFQ69G5FA1"


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


# ===================================================================
# SessionModel.to_domain()
# ===================================================================


@given("a r2mod-SessionModel with all JSON fields as None")
def step_session_none_json(context: Any) -> None:
    model = SessionModel(
        session_id=_ULID_SESS,
        actor_name=None,
        namespace="local",
        linked_plan_ids_json=None,
        token_usage_json=None,
        metadata_json=None,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    _set_rel_none(model, "messages_rel")
    context.r2_session_model = model


@when("I r2mod-convert the SessionModel to domain")
def step_session_to_domain(context: Any) -> None:
    context.r2_session_domain = context.r2_session_model.to_domain()


@then("the r2mod-session linked_plan_ids should be empty")
def step_session_plan_ids_empty(context: Any) -> None:
    assert context.r2_session_domain.linked_plan_ids == []


@then("the r2mod-session token_usage input_tokens should be 0")
def step_session_tokens_zero(context: Any) -> None:
    assert context.r2_session_domain.token_usage.input_tokens == 0


@then("the r2mod-session metadata should be empty")
def step_session_meta_empty(context: Any) -> None:
    assert context.r2_session_domain.metadata == {}


@then("the r2mod-session messages should be empty")
def step_session_messages_empty(context: Any) -> None:
    assert context.r2_session_domain.messages == []


# ===================================================================
# SessionMessageModel.to_domain()
# ===================================================================


@given("a r2mod-SessionMessageModel with None metadata_json")
def step_msg_none_meta(context: Any) -> None:
    context.r2_msg_model = SessionMessageModel(
        message_id=_ULID_MSG1,
        session_id=_ULID_SESS,
        role="user",
        content="hello",
        sequence=0,
        timestamp=_NOW_ISO,
        metadata_json=None,
        tool_call_id=None,
    )


@when("I r2mod-convert the SessionMessageModel to domain")
def step_msg_to_domain(context: Any) -> None:
    context.r2_msg_domain = context.r2_msg_model.to_domain()


@then("the r2mod-message metadata should be empty")
def step_msg_meta_empty(context: Any) -> None:
    assert context.r2_msg_domain.metadata == {}


# ===================================================================
# ToolModel.to_domain() / from_domain()
# ===================================================================


@given("a r2mod-ToolModel with no resource bindings")
def step_tool_no_bindings(context: Any) -> None:
    model = ToolModel(
        name="local/test-tool",
        namespace="local",
        short_name="test-tool",
        description="A test tool",
        tool_type="tool",
        source="builtin",
        input_schema_json=None,
        output_schema_json=None,
        capability_json=None,
        lifecycle_json=None,
        code=None,
        mcp_server=None,
        mcp_tool_name=None,
        agent_skill_path=None,
        timeout=300,
        wraps=None,
        transform=None,
        mode=None,
        argument_mapping_json=None,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    _set_rel_none(model, "resource_bindings_rel")
    model.validation_attachments_rel = []
    context.r2_tool_model = model


@when("I r2mod-convert the ToolModel to domain")
def step_tool_to_domain(context: Any) -> None:
    context.r2_tool_domain = context.r2_tool_model.to_domain()


@then("the r2mod-tool resource_bindings should be empty")
def step_tool_bindings_empty(context: Any) -> None:
    assert context.r2_tool_domain["resource_bindings"] == []


@when('I r2mod-create ToolModel from dict with name "{name}"')
def step_tool_from_dict(context: Any, name: str) -> None:
    context.r2_created_tool = ToolModel.from_domain({"name": name, "description": "d"})


@when('I r2mod-create ToolModel from object with name "{name}"')
def step_tool_from_obj(context: Any, name: str) -> None:
    obj = SimpleNamespace(name=name, description="d")
    context.r2_created_tool = ToolModel.from_domain(obj)


@when("I r2mod-create ToolModel from dict with no resource_bindings")
def step_tool_from_dict_no_bindings(context: Any) -> None:
    context.r2_created_tool = ToolModel.from_domain(
        {"name": "ns/t", "description": "d", "resource_bindings": None}
    )


@then("the r2mod-created ToolModel namespace should be empty string")
def step_tool_ns_empty(context: Any) -> None:
    assert context.r2_created_tool.namespace == ""


@then('the r2mod-created ToolModel namespace should be "{ns}"')
def step_tool_ns_value(context: Any, ns: str) -> None:
    assert context.r2_created_tool.namespace == ns


@then('the r2mod-created ToolModel short_name should be "{sn}"')
def step_tool_short_name(context: Any, sn: str) -> None:
    assert context.r2_created_tool.short_name == sn


@then("the r2mod-created ToolModel resource_bindings_rel should be empty")
def step_tool_created_bindings_empty(context: Any) -> None:
    assert len(context.r2_created_tool.resource_bindings_rel) == 0


# ===================================================================
# NamespacedProjectModel.to_domain()
# ===================================================================


@given('a r2mod-ProjectModel with no context_policy_json and name "{name}"')
def step_project_no_policy(context: Any, name: str) -> None:
    model = NamespacedProjectModel(
        namespaced_name=name,
        namespace="local",
        description=None,
        invariants_json=None,
        automation_profile=None,
        invariant_actor=None,
        context_policy_json=None,
        tags_json="[]",
        created_by=None,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    model.resource_links = []
    context.r2_project_model = model


@given("a r2mod-ProjectModel with None resource_links")
def step_project_none_links(context: Any) -> None:
    model = NamespacedProjectModel(
        namespaced_name="local/proj",
        namespace="local",
        description=None,
        invariants_json=None,
        automation_profile=None,
        invariant_actor=None,
        context_policy_json=None,
        tags_json="[]",
        created_by=None,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    _set_rel_none(model, "resource_links")
    context.r2_project_model = model


@when("I r2mod-convert the ProjectModel to domain")
def step_project_to_domain(context: Any) -> None:
    context.r2_project_domain = context.r2_project_model.to_domain()


@then('the r2mod-project name should be "{name}"')
def step_project_name(context: Any, name: str) -> None:
    assert context.r2_project_domain.name == name


@then("the r2mod-project linked_resources should be empty")
def step_project_links_empty(context: Any) -> None:
    assert context.r2_project_domain.linked_resources == []


# ===================================================================
# NamespacedProjectModel.from_domain()
# ===================================================================


@when("I r2mod-create ProjectModel from domain with None context_config")
def step_project_from_domain_none_config(context: Any) -> None:
    obj = SimpleNamespace(
        namespaced_name="local/proj",
        namespace="local",
        description="test",
        context_config=None,
        tags=[],
        created_at=datetime.now(tz=UTC),
        updated_at=datetime.now(tz=UTC),
    )
    context.r2_created_project = NamespacedProjectModel.from_domain(obj)


@then("the r2mod-created ProjectModel context_policy_json should be None")
def step_project_created_policy_none(context: Any) -> None:
    assert context.r2_created_project.context_policy_json is None


# ===================================================================
# SessionMessageModel.from_domain()
# ===================================================================


@when("I r2mod-create SessionMessageModel from domain with string role")
def step_msg_from_domain_string_role(context: Any) -> None:
    """Test the branch where role has no .value attr (plain string)."""
    obj = SimpleNamespace(
        message_id=_ULID_MSG1,
        session_id=_ULID_SESS,
        role="user",  # plain string, no .value
        content="hello",
        sequence=0,
        timestamp=datetime.now(tz=UTC),
        metadata={},
        tool_call_id=None,
    )
    context.r2_created_msg = SessionMessageModel.from_domain(obj)


@then('the r2mod-created SessionMessageModel role should be "{role}"')
def step_msg_created_role(context: Any, role: str) -> None:
    assert context.r2_created_msg.role == role
