"""Step definitions for features/context_fragment_models.feature.

Tests the ScoredFragment pipeline model and ContextFragment strategy_source
field directly in-memory -- no database required.
"""

from __future__ import annotations

from typing import Any

from behave import given, then, when
from behave.runner import Context
from pydantic import ValidationError

from cleveragents.domain.contexts.fragment import ScoredFragment
from cleveragents.domain.models.core.context_fragment import (
    ContextFragment,
    FragmentProvenance,
)

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

_DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://default")


def _make_fragment(**kwargs: Any) -> ContextFragment:
    """Create a ContextFragment with sensible test defaults."""
    kwargs.setdefault("uko_node", "test://default")
    kwargs.setdefault("content", "test content")
    kwargs.setdefault("token_count", 0)
    kwargs.setdefault("provenance", _DEFAULT_PROVENANCE)
    return ContextFragment(**kwargs)


# ---------------------------------------------------------------------------
# Given steps
# ---------------------------------------------------------------------------


@given(
    'a context fragment with uko_node "{uko}" and content "{content}"'
    " and token_count {tokens:d} and relevance {rel:g}"
)
def step_given_fragment_with_relevance(
    context: Context,
    uko: str,
    content: str,
    tokens: int,
    rel: float,
) -> None:
    context.fragment = _make_fragment(
        uko_node=uko,
        content=content,
        token_count=tokens,
        relevance_score=rel,
    )


@given(
    'a context fragment with uko_node "{uko}" and content "{content}"'
    ' and token_count {tokens:d} and strategy_source "{strategy}"'
)
def step_given_fragment_with_strategy(
    context: Context,
    uko: str,
    content: str,
    tokens: int,
    strategy: str,
) -> None:
    context.fragment = _make_fragment(
        uko_node=uko,
        content=content,
        token_count=tokens,
        strategy_source=strategy,
    )


@given('two scored fragments with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_two_same_scored_fragments(
    context: Context,
    uko: str,
    depth: int,
) -> None:
    frag_a = _make_fragment(
        uko_node=uko,
        content="content A",
        detail_depth=depth,
        token_count=10,
    )
    frag_b = _make_fragment(
        uko_node=uko,
        content="content B",
        detail_depth=depth,
        token_count=20,
    )
    context.scored_a = ScoredFragment.from_fragment(frag_a, composite_score=0.5)
    context.scored_b = ScoredFragment.from_fragment(frag_b, composite_score=0.9)


@given('a scored fragment with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_scored_fragment_first(
    context: Context,
    uko: str,
    depth: int,
) -> None:
    frag = _make_fragment(
        uko_node=uko,
        content="content",
        detail_depth=depth,
        token_count=10,
    )
    context.scored_a = ScoredFragment.from_fragment(frag, composite_score=0.5)


@given('another scored fragment with uko_node "{uko}" and detail_depth {depth:d}')
def step_given_scored_fragment_second(
    context: Context,
    uko: str,
    depth: int,
) -> None:
    frag = _make_fragment(
        uko_node=uko,
        content="content",
        detail_depth=depth,
        token_count=10,
    )
    context.scored_b = ScoredFragment.from_fragment(frag, composite_score=0.7)


@given(
    'a scored fragment with uko_node "{uko}" and detail_depth {depth:d}'
    " and score {score:g}"
)
def step_given_scored_fragment_with_score(
    context: Context,
    uko: str,
    depth: int,
    score: float,
) -> None:
    frag = _make_fragment(
        uko_node=uko,
        content="content",
        detail_depth=depth,
        token_count=10,
    )
    context.scored_a = ScoredFragment.from_fragment(frag, composite_score=score)


@given(
    'another scored fragment with uko_node "{uko}" and detail_depth {depth:d}'
    " and score {score:g}"
)
def step_given_another_scored_fragment_with_score(
    context: Context,
    uko: str,
    depth: int,
    score: float,
) -> None:
    frag = _make_fragment(
        uko_node=uko,
        content="content",
        detail_depth=depth,
        token_count=10,
    )
    context.scored_b = ScoredFragment.from_fragment(frag, composite_score=score)


@given('three scored fragments where two share uko_node "{uko}" detail_depth {depth:d}')
def step_given_three_scored_fragments(
    context: Context,
    uko: str,
    depth: int,
) -> None:
    frag_a = _make_fragment(
        uko_node=uko,
        content="content A",
        detail_depth=depth,
        token_count=10,
    )
    frag_b = _make_fragment(
        uko_node=uko,
        content="content B",
        detail_depth=depth,
        token_count=20,
    )
    frag_c = _make_fragment(
        uko_node="project://app/other.py",
        content="content C",
        detail_depth=depth,
        token_count=30,
    )
    context.scored_list = [
        ScoredFragment.from_fragment(frag_a, composite_score=0.5),
        ScoredFragment.from_fragment(frag_b, composite_score=0.9),
        ScoredFragment.from_fragment(frag_c, composite_score=0.7),
    ]


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when("I create a scored fragment with composite_score {score:g}")
def step_when_create_scored(context: Context, score: float) -> None:
    try:
        context.scored = ScoredFragment(
            fragment=context.fragment,
            composite_score=score,
        )
        context.error = None
    except (ValidationError, ValueError) as exc:
        context.scored = None
        context.error = exc


@when(
    "I create a scored fragment with composite_score {score:g}"
    " and rank {rank:d} and breakdown:"
)
def step_when_create_scored_with_breakdown(
    context: Context,
    score: float,
    rank: int,
) -> None:
    breakdown: dict[str, float] = {}
    assert context.table is not None
    for row in context.table:
        breakdown[row["component"]] = float(row["score"])
    context.scored = ScoredFragment(
        fragment=context.fragment,
        composite_score=score,
        score_breakdown=breakdown,
        rank=rank,
    )


@when("I create a scored fragment from relevance")
def step_when_create_from_relevance(context: Context) -> None:
    context.scored = ScoredFragment.from_relevance(context.fragment)


@when("I create a scored fragment from factory with score {score:g} and rank {rank:d}")
def step_when_create_from_factory(
    context: Context,
    score: float,
    rank: int,
) -> None:
    context.scored = ScoredFragment.from_fragment(
        context.fragment,
        composite_score=score,
        rank=rank,
    )


@when("I create a scored fragment with an invalid breakdown component {score:g}")
def step_when_create_invalid_breakdown(context: Context, score: float) -> None:
    try:
        context.scored = ScoredFragment(
            fragment=context.fragment,
            composite_score=0.5,
            score_breakdown={"bad": score},
        )
        context.error = None
    except (ValidationError, ValueError) as exc:
        context.scored = None
        context.error = exc


@when("I create a scored fragment with composite_score {score:g} and rank {rank:d}")
def step_when_create_scored_with_rank(
    context: Context,
    score: float,
    rank: int,
) -> None:
    try:
        context.scored = ScoredFragment(
            fragment=context.fragment,
            composite_score=score,
            rank=rank,
        )
        context.error = None
    except (ValidationError, ValueError) as exc:
        context.scored = None
        context.error = exc


@when("I add them to a set")
def step_when_add_to_set(context: Context) -> None:
    context.scored_set = set(context.scored_list)


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then("the scored fragment composite_score should be {expected:g}")
def step_then_composite_score(context: Context, expected: float) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.composite_score == expected, (
        f"Expected composite_score={expected}, got {context.scored.composite_score}"
    )


@then("the scored fragment rank should be {expected:d}")
def step_then_rank(context: Context, expected: int) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.rank == expected, (
        f"Expected rank={expected}, got {context.scored.rank}"
    )


@then("the scored fragment score_breakdown should be empty")
def step_then_breakdown_empty(context: Context) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.score_breakdown == {}, (
        f"Expected empty breakdown, got {context.scored.score_breakdown}"
    )


@then('the scored fragment uko_node should be "{expected}"')
def step_then_scored_uko_node(context: Context, expected: str) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.uko_node == expected, (
        f"Expected uko_node={expected}, got {context.scored.uko_node}"
    )


@then("the scored fragment detail_depth should be {expected:d}")
def step_then_scored_detail_depth(context: Context, expected: int) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.detail_depth == expected, (
        f"Expected detail_depth={expected}, got {context.scored.detail_depth}"
    )


@then("the scored fragment token_count should be {expected:d}")
def step_then_scored_token_count(context: Context, expected: int) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert context.scored.token_count == expected, (
        f"Expected token_count={expected}, got {context.scored.token_count}"
    )


@then('the scored fragment breakdown "{key}" should be {expected:g}')
def step_then_breakdown_value(context: Context, key: str, expected: float) -> None:
    assert context.scored is not None, "No scored fragment created"
    assert key in context.scored.score_breakdown, (
        f"Key '{key}' not in score_breakdown: {context.scored.score_breakdown}"
    )
    actual = context.scored.score_breakdown[key]
    assert actual == expected, f"Expected breakdown['{key}']={expected}, got {actual}"


@then("the two scored fragments should be equal")
def step_then_scored_equal(context: Context) -> None:
    assert context.scored_a == context.scored_b, "Expected scored fragments to be equal"


@then("the two scored fragments should not be equal")
def step_then_scored_not_equal(context: Context) -> None:
    assert context.scored_a != context.scored_b, (
        "Expected scored fragments to NOT be equal"
    )


@then("the two scored fragments should have the same hash")
def step_then_scored_same_hash(context: Context) -> None:
    assert hash(context.scored_a) == hash(context.scored_b), (
        "Expected scored fragments to have the same hash"
    )


@then("the set should contain {expected:d} unique scored fragments")
def step_then_set_size(context: Context, expected: int) -> None:
    assert len(context.scored_set) == expected, (
        f"Expected {expected} unique fragments, got {len(context.scored_set)}"
    )


@then("modifying the scored fragment composite_score should raise an error")
def step_then_frozen(context: Context) -> None:
    try:
        context.scored.composite_score = 0.99  # type: ignore[misc]
        raise AssertionError("Expected ValidationError for frozen model")
    except ValidationError:
        pass


@then("the fragment strategy_source should be empty")
def step_then_strategy_source_empty(context: Context) -> None:
    assert context.fragment.strategy_source == "", (
        f"Expected empty strategy_source, got '{context.fragment.strategy_source}'"
    )


@then('the fragment strategy_source should be "{expected}"')
def step_then_strategy_source(context: Context, expected: str) -> None:
    assert context.fragment.strategy_source == expected, (
        f"Expected strategy_source='{expected}', "
        f"got '{context.fragment.strategy_source}'"
    )
