"""Step definitions for models_skill_coverage_r2.feature.

Provides branch coverage for the ``SkillModel.to_domain()`` /
``SkillModel.from_domain()`` 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

import json
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 (
    SkillItemModel,
    SkillModel,
)

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

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


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_skill_model(
    *,
    items: list[SkillItemModel] | None = None,
    metadata_json: str | None = None,
) -> SkillModel:
    """Create a minimal ``SkillModel`` with controllable items/metadata."""
    model = SkillModel(
        name="test/skill",
        namespace="test",
        short_name="skill",
        description="A test skill",
        version=None,
        metadata_json=metadata_json,
        created_at=_NOW_ISO,
        updated_at=_NOW_ISO,
    )
    if items is not None:
        model.items_rel = items
    else:
        # Force None to test the ``or []`` fallback
        _set_rel_none(model, "items_rel")
    return model


def _make_skill_item(
    *,
    item_type: str,
    item_name: str = "dummy",
    item_config: str | None = None,
    item_order: int = 0,
) -> SkillItemModel:
    """Create a standalone ``SkillItemModel``."""
    return SkillItemModel(
        item_type=item_type,
        item_name=item_name,
        item_config=item_config,
        item_order=item_order,
        created_at=_NOW_ISO,
    )


# ===================================================================
# SkillModel.to_domain() - Given
# ===================================================================


@given("a r2mod-SkillModel with no items and no metadata")
def step_skill_no_items(context: Any) -> None:
    context.r2_skill_model = _make_skill_model(items=None, metadata_json=None)


@given('a r2mod-SkillModel with a tool_ref item named "{name}"')
def step_skill_tool_ref(context: Any, name: str) -> None:
    item = _make_skill_item(item_type="tool_ref", item_name=name)
    context.r2_skill_model = _make_skill_model(items=[item])


@given('a r2mod-SkillModel with an include item "{name}" with config overrides')
def step_skill_include_with_config(context: Any, name: str) -> None:
    config = json.dumps({"overrides": {"timeout": 60}})
    item = _make_skill_item(item_type="include", item_name=name, item_config=config)
    context.r2_skill_model = _make_skill_model(items=[item])


@given('a r2mod-SkillModel with an include item "{name}" without config')
def step_skill_include_no_config(context: Any, name: str) -> None:
    item = _make_skill_item(item_type="include", item_name=name, item_config=None)
    context.r2_skill_model = _make_skill_model(items=[item])


@given("a r2mod-SkillModel with an inline_tool item without config")
def step_skill_inline_no_config(context: Any) -> None:
    item = _make_skill_item(item_type="inline_tool", item_name="anon", item_config=None)
    context.r2_skill_model = _make_skill_model(items=[item])


@given("a r2mod-SkillModel with an mcp_source item without config")
def step_skill_mcp_no_config(context: Any) -> None:
    item = _make_skill_item(
        item_type="mcp_source", item_name="server1", item_config=None
    )
    context.r2_skill_model = _make_skill_model(items=[item])


@given('a r2mod-SkillModel with an agent_source item "{path}"')
def step_skill_agent_source(context: Any, path: str) -> None:
    item = _make_skill_item(item_type="agent_source", item_name=path)
    context.r2_skill_model = _make_skill_model(items=[item])


@given("a r2mod-SkillModel with metadata_json containing overrides")
def step_skill_with_metadata(context: Any) -> None:
    meta = json.dumps({"overrides": {"local/my-tool": {"timeout": 30}}})
    context.r2_skill_model = _make_skill_model(items=[], metadata_json=meta)


@given("a r2mod-SkillModel with inline_tool and mcp_source items with config")
def step_skill_inline_and_mcp_with_config(context: Any) -> None:
    inline_config = json.dumps({"description": "inline desc", "source": "custom"})
    mcp_config = json.dumps({"server": "mcp-srv", "tools": None, "env": None})
    items = [
        _make_skill_item(
            item_type="inline_tool",
            item_name="inline desc",
            item_config=inline_config,
            item_order=0,
        ),
        _make_skill_item(
            item_type="mcp_source",
            item_name="mcp-srv",
            item_config=mcp_config,
            item_order=1,
        ),
    ]
    context.r2_skill_model = _make_skill_model(items=items)


# ===================================================================
# SkillModel.to_domain() - When / Then
# ===================================================================


@when("I r2mod-convert the SkillModel to domain")
def step_skill_to_domain(context: Any) -> None:
    context.r2_skill_domain = context.r2_skill_model.to_domain()


@then("the r2mod-skill tool_refs should be empty")
def step_skill_tool_refs_empty(context: Any) -> None:
    assert context.r2_skill_domain.tool_refs == []


@then('the r2mod-skill tool_refs should contain "{name}"')
def step_skill_tool_refs_contains(context: Any, name: str) -> None:
    assert name in context.r2_skill_domain.tool_refs


@then("the r2mod-skill includes should be empty")
def step_skill_includes_empty(context: Any) -> None:
    assert context.r2_skill_domain.includes == []


@then("the r2mod-skill includes should have {n:d} entry")
def step_skill_includes_count(context: Any, n: int) -> None:
    assert len(context.r2_skill_domain.includes) == n


@then('the r2mod-skill first include name should be "{name}"')
def step_skill_first_include_name(context: Any, name: str) -> None:
    assert context.r2_skill_domain.includes[0].name == name


@then("the r2mod-skill first include overrides should not be None")
def step_skill_first_include_overrides_not_none(context: Any) -> None:
    assert context.r2_skill_domain.includes[0].overrides is not None


@then("the r2mod-skill first include overrides should be None")
def step_skill_first_include_overrides_none(context: Any) -> None:
    assert context.r2_skill_domain.includes[0].overrides is None


@then("the r2mod-skill anonymous_tools should be empty")
def step_skill_anon_empty(context: Any) -> None:
    assert context.r2_skill_domain.anonymous_tools == []


@then("the r2mod-skill anonymous_tools should have {n:d} entry")
def step_skill_anon_count(context: Any, n: int) -> None:
    assert len(context.r2_skill_domain.anonymous_tools) == n


@then("the r2mod-skill mcp_servers should be empty")
def step_skill_mcp_empty(context: Any) -> None:
    assert context.r2_skill_domain.mcp_servers == []


@then("the r2mod-skill mcp_servers should have {n:d} entry")
def step_skill_mcp_count(context: Any, n: int) -> None:
    assert len(context.r2_skill_domain.mcp_servers) == n


@then("the r2mod-skill agent_skills should be empty")
def step_skill_agents_empty(context: Any) -> None:
    assert context.r2_skill_domain.agent_skills == []


@then("the r2mod-skill agent_skills should have {n:d} entry")
def step_skill_agents_count(context: Any, n: int) -> None:
    assert len(context.r2_skill_domain.agent_skills) == n


@then('the r2mod-skill first agent_skill path should be "{path}"')
def step_skill_first_agent_path(context: Any, path: str) -> None:
    assert context.r2_skill_domain.agent_skills[0].path == path


@then("the r2mod-skill overrides should be empty")
def step_skill_overrides_empty(context: Any) -> None:
    assert context.r2_skill_domain.overrides == {}


@then("the r2mod-skill overrides should not be empty")
def step_skill_overrides_not_empty(context: Any) -> None:
    assert context.r2_skill_domain.overrides != {}


# ===================================================================
# SkillModel.from_domain() - When / Then
# ===================================================================


@when('I r2mod-attempt from_domain on SkillModel with name "{name}"')
def step_skill_from_domain_invalid(context: Any, name: str) -> None:
    try:
        SkillModel.from_domain({"name": name, "description": "d"})
        context.r2_error = None
    except ValueError as exc:
        context.r2_error = exc


@then("a r2mod-ValueError should have been raised")
def step_r2_valueerror(context: Any) -> None:
    assert context.r2_error is not None
    assert isinstance(context.r2_error, ValueError)


@when('I r2mod-create SkillModel from domain with name "{name}" and no overrides')
def step_skill_from_domain_no_overrides(context: Any, name: str) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {"name": name, "description": "d", "overrides": {}}
    )


@when('I r2mod-create SkillModel from domain with name "{name}" and overrides')
def step_skill_from_domain_with_overrides(context: Any, name: str) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": name,
            "description": "d",
            "overrides": {"local/tool": {"timeout": 10}},
        }
    )


@then("the r2mod-created SkillModel metadata_json should be None")
def step_skill_created_meta_none(context: Any) -> None:
    assert context.r2_created_skill.metadata_json is None


@then('the r2mod-created SkillModel metadata_json should contain "overrides"')
def step_skill_created_meta_overrides(context: Any) -> None:
    assert context.r2_created_skill.metadata_json is not None
    assert "overrides" in context.r2_created_skill.metadata_json


@when("I r2mod-create SkillModel from domain with includes as strings")
def step_skill_from_domain_string_includes(context: Any) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "includes": ["included-skill"],
        }
    )


@then("the r2mod-created SkillModel should have include items")
def step_skill_has_include_items(context: Any) -> None:
    include_items = [
        i for i in context.r2_created_skill.items_rel if i.item_type == "include"
    ]
    assert len(include_items) > 0


@when("I r2mod-create SkillModel from domain with anonymous_tools as dicts")
def step_skill_from_domain_anon_dicts(context: Any) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "anonymous_tools": [
                {"description": "dict tool", "source": "custom"},
            ],
        }
    )


@then("the r2mod-created SkillModel should have inline_tool items")
def step_skill_has_inline_items(context: Any) -> None:
    inline_items = [
        i for i in context.r2_created_skill.items_rel if i.item_type == "inline_tool"
    ]
    assert len(inline_items) > 0


@when("I r2mod-create SkillModel from domain with mcp_servers as dicts")
def step_skill_from_domain_mcp_dicts(context: Any) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "mcp_servers": [
                {"server": "my-mcp", "tools": None, "env": None},
            ],
        }
    )


@then("the r2mod-created SkillModel should have mcp_source items")
def step_skill_has_mcp_items(context: Any) -> None:
    mcp_items = [
        i for i in context.r2_created_skill.items_rel if i.item_type == "mcp_source"
    ]
    assert len(mcp_items) > 0


@when("I r2mod-create SkillModel from domain with agent_skills as strings")
def step_skill_from_domain_agent_strings(context: Any) -> None:
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "agent_skills": ["path/to/agent"],
        }
    )


@then("the r2mod-created SkillModel should have agent_source items")
def step_skill_has_agent_items(context: Any) -> None:
    agent_items = [
        i for i in context.r2_created_skill.items_rel if i.item_type == "agent_source"
    ]
    assert len(agent_items) > 0


@when("I r2mod-create SkillModel from domain with anonymous_tools as plain objects")
def step_skill_from_domain_anon_plain_obj(context: Any) -> None:
    """Test the fallback else branch (not model_dump, not dict)."""
    obj = SimpleNamespace(description="plain obj tool", source="custom")
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "anonymous_tools": [obj],
        }
    )


@when("I r2mod-create SkillModel from domain with mcp_servers as plain objects")
def step_skill_from_domain_mcp_plain_obj(context: Any) -> None:
    """Test the fallback else branch (not model_dump, not dict)."""
    obj = SimpleNamespace(server="my-mcp")
    context.r2_created_skill = SkillModel.from_domain(
        {
            "name": "ns/sk",
            "description": "d",
            "mcp_servers": [obj],
        }
    )
