"""Step definitions for Skill Registry flattened tool persistence.

Covers the m4_002 columns: ``flattened_tools_json``, ``includes_json``,
``capability_summary_json``, ``yaml_text``, ``flattening_hash`` and
the related ``SkillRepository`` helper methods.
"""

from __future__ import annotations

from collections.abc import Callable
from typing import Any

from behave import given, then, when
from behave.runner import Context
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, sessionmaker

from cleveragents.domain.models.core.skill import Skill
from cleveragents.infrastructure.database.models import Base
from cleveragents.infrastructure.database.repositories import (
    DuplicateSkillError,
    SkillRepository,
)

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


def _enable_fk_pragma(dbapi_conn: Any, _connection_record: Any) -> None:
    """Enable SQLite foreign key enforcement per connection."""
    cursor = dbapi_conn.cursor()
    cursor.execute("PRAGMA foreign_keys=ON")
    cursor.close()


def _factory(context: Context) -> Callable[[], Session]:
    return context.sp_session_factory  # type: ignore[no-any-return]


def _commit(context: Context) -> None:
    _factory(context)().commit()


def _make_skill(name: str, description: str = "Test skill") -> Skill:
    return Skill(name=name, description=description)


# ---------------------------------------------------------------------------
# Background
# ---------------------------------------------------------------------------


@given("a clean in-memory database with the skill persistence schema")
def step_clean_db(context: Context) -> None:
    engine = create_engine("sqlite:///:memory:", echo=False)
    event.listen(engine, "connect", _enable_fk_pragma)
    Base.metadata.create_all(engine)

    shared_session: Session = sessionmaker(bind=engine)()

    def _sf() -> Session:
        return shared_session

    context.sp_engine = engine
    context.sp_session_factory = _sf
    context.sp_error: BaseException | None = None
    context.sp_recomputed_hash: str | None = None
    context.sp_listed: list[Skill] = []


@given("a skill persistence repository")
def step_repo(context: Context) -> None:
    context.sp_repo = SkillRepository(session_factory=_factory(context))


# ---------------------------------------------------------------------------
# Skill creation
# ---------------------------------------------------------------------------


@given('a skill "{name}" with description "{desc}"')
def step_skill(context: Context, name: str, desc: str) -> None:
    context.sp_current_skill = _make_skill(name, desc)


@when("the skill is created via the persistence repository")
def step_create(context: Context) -> None:
    context.sp_repo.create(context.sp_current_skill)
    _commit(context)


@given("the skill is created via the persistence repository")
def step_create_given(context: Context) -> None:
    context.sp_repo.create(context.sp_current_skill)
    _commit(context)


# ---------------------------------------------------------------------------
# Flattened tools update
# ---------------------------------------------------------------------------


@when("the flattened tools are updated with tools json '{tools}' and hash \"{h}\"")
def step_update_flat(context: Context, tools: str, h: str) -> None:
    name = context.sp_current_skill.name
    context.sp_repo.update_flattened_tools(
        name=name,
        flattened_tools_json=tools,
        includes_json="[]",
        capability_summary_json="{}",
        yaml_text="name: " + name,
        flattening_hash=h,
    )
    _commit(context)


@given("the flattened tools are updated with tools json '{tools}' and hash \"{h}\"")
def step_update_flat_given(context: Context, tools: str, h: str) -> None:
    name = context.sp_current_skill.name
    context.sp_repo.update_flattened_tools(
        name=name,
        flattened_tools_json=tools,
        includes_json="[]",
        capability_summary_json="{}",
        yaml_text="name: " + name,
        flattening_hash=h,
    )
    _commit(context)


# ---------------------------------------------------------------------------
# Assertions on stored fields
# ---------------------------------------------------------------------------


@then("the stored flattened tools json should be '{expected}'")
def step_check_tools(context: Context, expected: str) -> None:
    data = context.sp_repo.get_flattened_tools(context.sp_current_skill.name)
    assert data["flattened_tools_json"] == expected, (
        f"Expected {expected!r}, got {data['flattened_tools_json']!r}"
    )


@then('the stored flattening hash should be "{expected}"')
def step_check_hash(context: Context, expected: str) -> None:
    data = context.sp_repo.get_flattened_tools(context.sp_current_skill.name)
    assert data["flattening_hash"] == expected, (
        f"Expected {expected!r}, got {data['flattening_hash']!r}"
    )


# ---------------------------------------------------------------------------
# Update & invalidation
# ---------------------------------------------------------------------------


@when('the skill "{name}" is updated with description "{desc}"')
def step_update_skill(context: Context, name: str, desc: str) -> None:
    updated = _make_skill(name, desc)
    context.sp_repo.update(updated)
    _commit(context)


@then('the stored flattened tools json for "{name}" should be null')
def step_check_tools_null(context: Context, name: str) -> None:
    data = context.sp_repo.get_flattened_tools(name)
    assert data["flattened_tools_json"] is None, (
        f"Expected None, got {data['flattened_tools_json']!r}"
    )


@then('the stored capability summary json for "{name}" should be null')
def step_check_cap_null(context: Context, name: str) -> None:
    data = context.sp_repo.get_flattened_tools(name)
    assert data["capability_summary_json"] is None, (
        f"Expected None, got {data['capability_summary_json']!r}"
    )


@then('the stored flattening hash for "{name}" should be null')
def step_check_hash_null(context: Context, name: str) -> None:
    data = context.sp_repo.get_flattened_tools(name)
    assert data["flattening_hash"] is None, (
        f"Expected None, got {data['flattening_hash']!r}"
    )


# ---------------------------------------------------------------------------
# Hash-based staleness
# ---------------------------------------------------------------------------


@then('the skill "{name}" should not need refresh for hash "{h}"')
def step_no_refresh(context: Context, name: str, h: str) -> None:
    assert not context.sp_repo.needs_refresh(name, h)


@then('the skill "{name}" should need refresh for hash "{h}"')
def step_needs_refresh(context: Context, name: str, h: str) -> None:
    assert context.sp_repo.needs_refresh(name, h)


# ---------------------------------------------------------------------------
# Recompute
# ---------------------------------------------------------------------------


@when('the flattening hash for "{name}" is recomputed from yaml "{yaml}"')
def step_recompute(context: Context, name: str, yaml: str) -> None:
    context.sp_recomputed_hash = context.sp_repo.recompute_flattening_hash(name, yaml)
    _commit(context)


@then('the stored flattening hash for "{name}" should not be null')
def step_hash_not_null(context: Context, name: str) -> None:
    data = context.sp_repo.get_flattened_tools(name)
    assert data["flattening_hash"] is not None


@then('the skill "{name}" should not need refresh for the recomputed hash')
def step_no_refresh_recomputed(context: Context, name: str) -> None:
    h = context.sp_recomputed_hash
    assert h is not None, "Recomputed hash is None"
    assert not context.sp_repo.needs_refresh(name, h)


# ---------------------------------------------------------------------------
# Uniqueness constraint
# ---------------------------------------------------------------------------


@when('a second skill "{name}" with description "{desc}" is created')
def step_create_duplicate(context: Context, name: str, desc: str) -> None:
    try:
        context.sp_repo.create(_make_skill(name, desc))
        _commit(context)
        context.sp_error = None
    except DuplicateSkillError as exc:
        context.sp_error = exc
    except Exception as exc:
        cause = exc.__cause__ or exc
        if isinstance(cause, DuplicateSkillError):
            context.sp_error = cause
        else:
            context.sp_error = exc


@then("the second creation should raise a duplicate error")
def step_check_dup(context: Context) -> None:
    assert isinstance(context.sp_error, DuplicateSkillError), (
        f"Expected DuplicateSkillError, got {type(context.sp_error)}"
    )


# ---------------------------------------------------------------------------
# Namespace filtering
# ---------------------------------------------------------------------------


@when('skills are listed with namespace "{ns}"')
def step_list_ns(context: Context, ns: str) -> None:
    context.sp_listed = context.sp_repo.list_all(namespace=ns)


@then('the listed skills should contain "{name}"')
def step_list_contains(context: Context, name: str) -> None:
    names = [s.name for s in context.sp_listed]
    assert name in names, f"{name} not in {names}"


@then('the listed skills should not contain "{name}"')
def step_list_not_contains(context: Context, name: str) -> None:
    names = [s.name for s in context.sp_listed]
    assert name not in names, f"{name} unexpectedly in {names}"
