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

Tests the ACMS core pipeline components directly in-memory.
"""

from __future__ import annotations

from datetime import UTC, datetime
from typing import Any

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

from cleveragents.application.services.acms_core_pipeline import (
    ActorPhaseStrategySelector,
    ConstrainedKnapsackPacker,
    PriorityCoherenceOrderer,
    RelevanceRecencyPriorityScorer,
    SpecBudgetAllocator,
)
from cleveragents.application.services.acms_service import (
    ACMSPipeline,
    StrategyCapabilities,
)
from cleveragents.domain.models.core.context_fragment import (
    ContextBudget,
    ContextFragment,
    FragmentProvenance,
)
from cleveragents.domain.models.core.context_policy import ContextView

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

_DEFAULT_PROVENANCE = FragmentProvenance(resource_uri="test://core-pipeline")
_TEST_PLAN_ID = "01JQTESTCP00000000000000CC"


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


class _MockStrategy:
    """A simple mock strategy for testing."""

    def __init__(
        self,
        name: str,
        confidence: float,
        quality_score: float = 1.0,
    ) -> None:
        self._name = name
        self._confidence = confidence
        self._quality_score = quality_score

    @property
    def name(self) -> str:
        return self._name

    @property
    def capabilities(self) -> StrategyCapabilities:
        return StrategyCapabilities(quality_score=self._quality_score)

    def can_handle(self, request: dict[str, Any]) -> float:
        return self._confidence

    def assemble(self, fragments: Any, budget: Any) -> list[Any]:
        return list(fragments)

    def explain(self) -> str:
        return f"Mock strategy: {self._name}"


# ---------------------------------------------------------------------------
# ActorPhaseStrategySelector — Given steps
# ---------------------------------------------------------------------------


@given("a set of core pipeline strategies")
def step_given_core_strategies(context: Context) -> None:
    context.core_strategies = [
        _MockStrategy("relevance", 0.8),
        _MockStrategy("tiered", 0.7),
        _MockStrategy("recency", 0.6),
        _MockStrategy("arce", 0.95),
    ]


@given("a set of core pipeline strategies including a zero-confidence strategy")
def step_given_core_strategies_with_zero(context: Context) -> None:
    context.core_strategies = [
        _MockStrategy("relevance", 0.8),
        _MockStrategy("zero-strategy", 0.0),
    ]


@given("an empty strategy list")
def step_given_empty_strategy_list(context: Context) -> None:
    context.core_strategies = []


@given("a strategy with confidence {conf:g}")
def step_given_strategy_with_confidence(context: Context, conf: float) -> None:
    context.core_strategies = [
        _MockStrategy("relevance", conf),
    ]


# ---------------------------------------------------------------------------
# ActorPhaseStrategySelector — When steps
# ---------------------------------------------------------------------------


@when("I select strategies with no actor_type or plan_phase")
def step_select_no_context(context: Context) -> None:
    selector = ActorPhaseStrategySelector()
    context.selector_result = selector.select(context.core_strategies, {})


@when('I select strategies with actor_type "{actor_type}" only')
def step_select_with_actor_only(context: Context, actor_type: str) -> None:
    selector = ActorPhaseStrategySelector()
    context.selector_result = selector.select(
        context.core_strategies, {"actor_type": actor_type}
    )


@when('I select strategies with plan_phase "{plan_phase}" only')
def step_select_with_phase_only(context: Context, plan_phase: str) -> None:
    selector = ActorPhaseStrategySelector()
    context.selector_result = selector.select(
        context.core_strategies, {"plan_phase": plan_phase}
    )


@when(
    'I select strategies with actor_type "{actor_type}" '
    'and plan_phase "{plan_phase_val}"'
)
def step_select_with_actor_and_phase(
    context: Context, actor_type: str, plan_phase_val: str
) -> None:
    selector = ActorPhaseStrategySelector()
    context.selector_result = selector.select(
        context.core_strategies,
        {"actor_type": actor_type, "plan_phase": plan_phase_val},
    )


# ---------------------------------------------------------------------------
# ActorPhaseStrategySelector — Then steps
# ---------------------------------------------------------------------------


@then("all core strategies with positive confidence should be returned")
def step_all_positive_confidence(context: Context) -> None:
    result_names = {s.name for s, _ in context.selector_result}
    for strategy in context.core_strategies:
        if strategy.can_handle({}) > 0.0:
            assert strategy.name in result_names, f"Expected {strategy.name} in results"


@then("the core strategies should be sorted by confidence descending")
def step_sorted_by_confidence(context: Context) -> None:
    confidences = [c for _, c in context.selector_result]
    assert confidences == sorted(confidences, reverse=True), (
        f"Not sorted: {confidences}"
    )


@then('the "{name}" strategy should have boosted confidence')
def step_strategy_boosted(context: Context, name: str) -> None:
    for strategy, confidence in context.selector_result:
        if strategy.name == name:
            base = strategy.can_handle({})
            assert confidence > base, (
                f"Expected {name} confidence > {base}, got {confidence}"
            )
            return
    msg = f"Strategy {name} not found in results"
    raise AssertionError(msg)


@then('the "{name}" strategy should have maximum boosted confidence')
def step_strategy_max_boosted(context: Context, name: str) -> None:
    for strategy, confidence in context.selector_result:
        if strategy.name == name:
            base = strategy.can_handle({})
            assert confidence > base, (
                f"Expected {name} confidence > {base}, got {confidence}"
            )
            assert confidence <= 1.0, f"Confidence {confidence} exceeds 1.0"
            return
    msg = f"Strategy {name} not found in results"
    raise AssertionError(msg)


@then("the core zero-confidence strategy should not be in the results")
def step_zero_confidence_excluded(context: Context) -> None:
    result_names = {s.name for s, _ in context.selector_result}
    assert "zero-strategy" not in result_names, (
        "Zero-confidence strategy should be excluded"
    )


@then("{count:d} strategies should be selected")
def step_strategies_count(context: Context, count: int) -> None:
    assert len(context.selector_result) == count, (
        f"Expected {count}, got {len(context.selector_result)}"
    )


@then("the strategy confidence should not exceed 1.0")
def step_confidence_not_exceed_one(context: Context) -> None:
    for _, confidence in context.selector_result:
        assert confidence <= 1.0, f"Confidence {confidence} exceeds 1.0"


# ---------------------------------------------------------------------------
# SpecBudgetAllocator — Given steps
# ---------------------------------------------------------------------------


@given("an empty candidate list")
def step_given_empty_candidates(context: Context) -> None:
    context.budget_candidates: list[tuple[Any, float]] = []


@given("a single candidate with confidence {conf:g}")
def step_given_single_candidate(context: Context, conf: float) -> None:
    context.budget_candidates = [(_MockStrategy("relevance", conf), conf)]


@given("two candidates with equal confidence {conf:g}")
def step_given_two_equal_candidates(context: Context, conf: float) -> None:
    context.budget_candidates = [
        (_MockStrategy("relevance", conf), conf),
        (_MockStrategy("tiered", conf), conf),
    ]


@given("two candidates with different quality scores")
def step_given_two_different_quality(context: Context) -> None:
    context.budget_candidates = [
        (_MockStrategy("high-quality", 0.8, quality_score=0.9), 0.8),
        (_MockStrategy("low-quality", 0.8, quality_score=0.1), 0.8),
    ]


@given("two candidates with zero confidence")
def step_given_two_zero_confidence(context: Context) -> None:
    context.budget_candidates = [
        (_MockStrategy("a", 0.0), 0.0),
        (_MockStrategy("b", 0.0), 0.0),
    ]


@given("three candidates where one would receive very few tokens")
def step_given_three_candidates_one_small(context: Context) -> None:
    context.budget_candidates = [
        (_MockStrategy("big-a", 0.9), 0.9),
        (_MockStrategy("big-b", 0.8), 0.8),
        (_MockStrategy("tiny", 0.01), 0.01),
    ]


@given("three candidates with varying confidence")
def step_given_three_varying_candidates(context: Context) -> None:
    context.budget_candidates = [
        (_MockStrategy("a", 0.9), 0.9),
        (_MockStrategy("b", 0.6), 0.6),
        (_MockStrategy("c", 0.3), 0.3),
    ]


# ---------------------------------------------------------------------------
# SpecBudgetAllocator — When steps
# ---------------------------------------------------------------------------


@when("I allocate budget {budget:d} with SpecBudgetAllocator")
def step_allocate_budget(context: Context, budget: int) -> None:
    allocator = SpecBudgetAllocator()
    context.allocation_result = allocator.allocate(context.budget_candidates, budget)


@when(
    "I allocate budget {budget:d} with SpecBudgetAllocator "
    "and min_useful_budget {min_budget:d}"
)
def step_allocate_budget_with_min(
    context: Context, budget: int, min_budget: int
) -> None:
    allocator = SpecBudgetAllocator(min_useful_budget=min_budget)
    context.allocation_result = allocator.allocate(context.budget_candidates, budget)


# ---------------------------------------------------------------------------
# SpecBudgetAllocator — Then steps
# ---------------------------------------------------------------------------


@then("{count:d} allocation should be returned")
def step_allocations_count_singular(context: Context, count: int) -> None:
    assert len(context.allocation_result) == count, (
        f"Expected {count}, got {len(context.allocation_result)}"
    )


@then("{count:d} allocations should be returned")
def step_allocations_count_plural(context: Context, count: int) -> None:
    assert len(context.allocation_result) == count, (
        f"Expected {count}, got {len(context.allocation_result)}"
    )


@then("the single allocation should receive {tokens:d} tokens")
def step_single_allocation_tokens(context: Context, tokens: int) -> None:
    assert len(context.allocation_result) == 1
    _, _, allocated = context.allocation_result[0]
    assert allocated == tokens, f"Expected {tokens}, got {allocated}"


@then("the total allocated tokens should equal {total:d}")
def step_total_allocated_tokens(context: Context, total: int) -> None:
    actual = sum(t for _, _, t in context.allocation_result)
    assert actual == total, f"Expected {total}, got {actual}"


@then("the higher quality candidate should receive more tokens")
def step_higher_quality_more_tokens(context: Context) -> None:
    assert len(context.allocation_result) == 2
    _, _, high_tokens = context.allocation_result[0]
    _, _, low_tokens = context.allocation_result[1]
    assert high_tokens > low_tokens, (
        f"Expected high-quality ({high_tokens}) > low-quality ({low_tokens})"
    )


@then("the small candidate should be excluded from allocations")
def step_small_candidate_excluded(context: Context) -> None:
    names = {s.name for s, _, _ in context.allocation_result}
    assert "tiny" not in names, f"Expected 'tiny' to be excluded, got {names}"


# ---------------------------------------------------------------------------
# RelevanceRecencyPriorityScorer — Given steps
# ---------------------------------------------------------------------------


@given("an empty core pipeline fragment list")
def step_given_empty_core_fragments(context: Context) -> None:
    context.core_fragments: list[ContextFragment] = []


@given("a core pipeline fragment with relevance {rel:g}")
def step_given_core_fragment_relevance(context: Context, rel: float) -> None:
    context.core_fragments = [_make_core_fragment(relevance_score=rel)]


@given("a core pipeline fragment with relevance {rel:g} and priority {pri:g}")
def step_given_core_fragment_relevance_priority(
    context: Context, rel: float, pri: float
) -> None:
    context.core_fragments = [
        _make_core_fragment(
            relevance_score=rel,
            metadata={"priority": str(pri)},
        )
    ]


@given("two identical core pipeline fragments")
def step_given_two_identical_fragments(context: Context) -> None:
    ts = datetime(2024, 1, 1, tzinfo=UTC)
    frag = _make_core_fragment(relevance_score=0.7, created_at=ts)
    context.core_fragments = [frag]
    context.core_fragments2 = [frag]


@given("two core pipeline fragments with different timestamps")
def step_given_two_different_timestamps(context: Context) -> None:
    ts_old = datetime(2024, 1, 1, tzinfo=UTC)
    ts_new = datetime(2024, 6, 1, tzinfo=UTC)
    context.core_fragments = [
        _make_core_fragment(
            uko_node="test://old",
            relevance_score=0.5,
            created_at=ts_old,
        ),
        _make_core_fragment(
            uko_node="test://new",
            relevance_score=0.5,
            created_at=ts_new,
        ),
    ]


@given("two core pipeline fragments with identical timestamps")
def step_given_two_identical_timestamps(context: Context) -> None:
    ts = datetime(2024, 1, 1, tzinfo=UTC)
    context.core_fragments = [
        _make_core_fragment(
            uko_node="test://a",
            relevance_score=0.5,
            created_at=ts,
        ),
        _make_core_fragment(
            uko_node="test://b",
            relevance_score=0.7,
            created_at=ts,
        ),
    ]


@given("a core pipeline fragment with maximum relevance and priority")
def step_given_max_relevance_priority(context: Context) -> None:
    context.core_fragments = [
        _make_core_fragment(
            relevance_score=1.0,
            metadata={"priority": "1.0"},
        )
    ]


@given("a core pipeline fragment with zero relevance and zero priority")
def step_given_zero_relevance_priority(context: Context) -> None:
    context.core_fragments = [
        _make_core_fragment(
            relevance_score=0.0,
            metadata={"priority": "0.0"},
        )
    ]


@given(
    "scorer configured with relevance_weight {rel:g} recency_weight {rec:g} "
    "priority_weight {pri:g}"
)
def step_given_custom_scorer_weights(
    context: Context, rel: float, rec: float, pri: float
) -> None:
    context.custom_scorer = RelevanceRecencyPriorityScorer(
        relevance_weight=rel,
        recency_weight=rec,
        priority_weight=pri,
    )


@given('a core pipeline fragment with metadata priority "{priority}"')
def step_given_fragment_with_priority_metadata(context: Context, priority: str) -> None:
    context.core_fragments = [
        _make_core_fragment(
            relevance_score=0.5,
            metadata={"priority": priority},
        )
    ]


@given("a core pipeline fragment with no priority metadata")
def step_given_fragment_no_priority(context: Context) -> None:
    context.core_fragments = [_make_core_fragment(relevance_score=0.5)]


# ---------------------------------------------------------------------------
# RelevanceRecencyPriorityScorer — When steps
# ---------------------------------------------------------------------------


@when("I score the fragments with RelevanceRecencyPriorityScorer")
def step_score_with_rrp_scorer(context: Context) -> None:
    scorer = RelevanceRecencyPriorityScorer()
    context.core_scored = list(scorer.score(context.core_fragments))


@when("I score both fragment sets with RelevanceRecencyPriorityScorer")
def step_score_both_sets(context: Context) -> None:
    scorer = RelevanceRecencyPriorityScorer()
    context.core_scored = list(scorer.score(context.core_fragments))
    context.core_scored2 = list(scorer.score(context.core_fragments2))


@when("I score the fragments with custom RelevanceRecencyPriorityScorer")
def step_score_with_custom_scorer(context: Context) -> None:
    scorer = context.custom_scorer
    context.core_scored = list(scorer.score(context.core_fragments))


# ---------------------------------------------------------------------------
# RelevanceRecencyPriorityScorer — Then steps
# ---------------------------------------------------------------------------


@then("{count:d} core scored fragments should be returned")
def step_scored_count_core(context: Context, count: int) -> None:
    assert len(context.core_scored) == count, (
        f"Expected {count}, got {len(context.core_scored)}"
    )


@then('the scored fragment should have _original_relevance metadata "{val}"')
def step_scored_original_relevance_core(context: Context, val: str) -> None:
    assert len(context.core_scored) >= 1
    meta = context.core_scored[0].metadata
    assert meta.get("_original_relevance") == val, (
        f"Expected {val}, got {meta.get('_original_relevance')}"
    )


@then("the scored fragment composite score should be between 0.0 and 1.0")
def step_scored_composite_range(context: Context) -> None:
    assert len(context.core_scored) >= 1
    score = context.core_scored[0].relevance_score
    assert 0.0 <= score <= 1.0, f"Score {score} out of range"


@then("both scored fragments should have identical composite scores")
def step_scored_identical(context: Context) -> None:
    assert len(context.core_scored) >= 1
    assert len(context.core_scored2) >= 1
    assert (
        context.core_scored[0].relevance_score
        == context.core_scored2[0].relevance_score
    )


@then("the newer fragment should have higher recency score")
def step_newer_higher_recency(context: Context) -> None:
    assert len(context.core_scored) == 2
    old_recency = float(context.core_scored[0].metadata.get("_score_recency", "0"))
    new_recency = float(context.core_scored[1].metadata.get("_score_recency", "0"))
    assert new_recency > old_recency, (
        f"Expected new ({new_recency}) > old ({old_recency})"
    )


@then("all scored fragments should have recency score 1.0")
def step_all_recency_one(context: Context) -> None:
    for frag in context.core_scored:
        recency = float(frag.metadata.get("_score_recency", "0"))
        assert abs(recency - 1.0) < 1e-6, f"Expected recency 1.0, got {recency}"


@then("the scored fragment composite score should be at most 1.0")
def step_scored_at_most_one(context: Context) -> None:
    assert len(context.core_scored) >= 1
    assert context.core_scored[0].relevance_score <= 1.0


@then("the scored fragment composite score should be at least 0.0")
def step_scored_at_least_zero(context: Context) -> None:
    assert len(context.core_scored) >= 1
    assert context.core_scored[0].relevance_score >= 0.0


@then("the scored fragment composite score should be {expected:g}")
def step_scored_exact_composite(context: Context, expected: float) -> None:
    assert len(context.core_scored) >= 1
    actual = context.core_scored[0].relevance_score
    assert abs(actual - expected) < 1e-4, f"Expected {expected}, got {actual}"


@then('the scored fragment should have _score_priority metadata near "{val}"')
def step_scored_priority_near(context: Context, val: str) -> None:
    assert len(context.core_scored) >= 1
    meta = context.core_scored[0].metadata
    actual = float(meta.get("_score_priority", "0"))
    expected = float(val)
    assert abs(actual - expected) < 0.01, f"Expected ~{expected}, got {actual}"


@then('the scored fragment should have _score_priority metadata "{val}"')
def step_scored_priority_exact(context: Context, val: str) -> None:
    assert len(context.core_scored) >= 1
    meta = context.core_scored[0].metadata
    actual = float(meta.get("_score_priority", "0"))
    expected = float(val)
    assert abs(actual - expected) < 0.01, f"Expected {expected}, got {actual}"


# ---------------------------------------------------------------------------
# ConstrainedKnapsackPacker — Given steps
# ---------------------------------------------------------------------------


@given("the following core pipeline fragments:")
def step_given_core_pipeline_fragments(context: Context) -> None:
    frags: list[ContextFragment] = []
    for row in context.table:
        frags.append(
            _make_core_fragment(
                uko_node=row["uko_node"],
                content=row["content"],
                relevance_score=float(row["score"]),
                token_count=int(row["tokens"]),
                detail_depth=int(row["depth"]),
            )
        )
    context.core_fragments = frags


@given("the following core pipeline fragments with priorities:")
def step_given_core_fragments_with_priorities(context: Context) -> None:
    frags: list[ContextFragment] = []
    for row in context.table:
        frags.append(
            _make_core_fragment(
                uko_node=row["uko_node"],
                content=row["content"],
                relevance_score=float(row["score"]),
                token_count=int(row["tokens"]),
                detail_depth=int(row["depth"]),
                metadata={"priority": row["priority"]},
            )
        )
    context.core_fragments = frags


@given("a core pipeline budget with max_tokens {max_t:d} and reserved_tokens {res_t:d}")
def step_given_core_budget(context: Context, max_t: int, res_t: int) -> None:
    context.core_budget = ContextBudget(max_tokens=max_t, reserved_tokens=res_t)


@given("a ConstrainedKnapsackPacker with max_file_size {max_size:d}")
def step_given_packer_max_file_size(context: Context, max_size: int) -> None:
    context.core_packer = ConstrainedKnapsackPacker(max_file_size=max_size)


@given("a ConstrainedKnapsackPacker with max_total_size {max_size:d}")
def step_given_packer_max_total_size(context: Context, max_size: int) -> None:
    context.core_packer = ConstrainedKnapsackPacker(max_total_size=max_size)


@given("a ConstrainedKnapsackPacker with context_view max_total_size {max_size:d}")
def step_given_packer_context_view(context: Context, max_size: int) -> None:
    view = ContextView(max_total_size=max_size)
    context.core_packer = ConstrainedKnapsackPacker(context_view=view)


# ---------------------------------------------------------------------------
# ConstrainedKnapsackPacker — When steps
# ---------------------------------------------------------------------------


@when("I pack the fragments with ConstrainedKnapsackPacker")
def step_pack_with_constrained_packer(context: Context) -> None:
    packer = getattr(context, "core_packer", ConstrainedKnapsackPacker())
    budget = getattr(
        context, "core_budget", ContextBudget(max_tokens=100000, reserved_tokens=0)
    )
    context.core_packed = list(packer.pack(context.core_fragments, budget))


# ---------------------------------------------------------------------------
# ConstrainedKnapsackPacker — Then steps
# ---------------------------------------------------------------------------


@then("{count:d} core packed fragment should be returned")
def step_packed_count_singular_core(context: Context, count: int) -> None:
    assert len(context.core_packed) == count, (
        f"Expected {count}, got {len(context.core_packed)}"
    )


@then("{count:d} core packed fragments should be returned")
def step_packed_count_plural_core(context: Context, count: int) -> None:
    assert len(context.core_packed) == count, (
        f"Expected {count}, got {len(context.core_packed)}"
    )


@then("the core packed total tokens should be at most {max_tokens:d}")
def step_packed_total_at_most_core(context: Context, max_tokens: int) -> None:
    total = sum(f.token_count for f in context.core_packed)
    assert total <= max_tokens, f"Expected <= {max_tokens}, got {total}"


@then('the core packed fragment uko_node should be "{expected}"')
def step_packed_single_node_core(context: Context, expected: str) -> None:
    assert len(context.core_packed) >= 1
    assert context.core_packed[0].uko_node == expected, (
        f"Expected {expected}, got {context.core_packed[0].uko_node}"
    )


# ---------------------------------------------------------------------------
# PriorityCoherenceOrderer — When steps
# ---------------------------------------------------------------------------


@when("I order the fragments with PriorityCoherenceOrderer")
def step_order_with_priority_orderer(context: Context) -> None:
    orderer = PriorityCoherenceOrderer()
    context.core_ordered = list(orderer.order(context.core_fragments))


# ---------------------------------------------------------------------------
# PriorityCoherenceOrderer — Then steps
# ---------------------------------------------------------------------------


@then("{count:d} core ordered fragment should be returned")
def step_ordered_count_singular(context: Context, count: int) -> None:
    assert len(context.core_ordered) == count, (
        f"Expected {count}, got {len(context.core_ordered)}"
    )


@then("{count:d} core ordered fragments should be returned")
def step_ordered_count_plural(context: Context, count: int) -> None:
    assert len(context.core_ordered) == count, (
        f"Expected {count}, got {len(context.core_ordered)}"
    )


@then('the first core ordered fragment should have uko_node "{expected}"')
def step_first_ordered_node(context: Context, expected: str) -> None:
    assert len(context.core_ordered) >= 1
    actual = context.core_ordered[0].uko_node
    assert actual == expected, f"Expected {expected}, got {actual}"


@then('fragments from "{prefix}" should be adjacent')
def step_fragments_adjacent(context: Context, prefix: str) -> None:
    matching_indices = [
        i for i, f in enumerate(context.core_ordered) if f.uko_node.startswith(prefix)
    ]
    if len(matching_indices) <= 1:
        return
    for i in range(len(matching_indices) - 1):
        assert matching_indices[i + 1] == matching_indices[i] + 1, (
            f"Fragments from {prefix} are not adjacent: {matching_indices}"
        )


# ---------------------------------------------------------------------------
# Pipeline Integration — Given steps
# ---------------------------------------------------------------------------


@given("the ACMS pipeline with an ActorPhaseStrategySelector")
def step_pipeline_with_actor_selector(context: Context) -> None:
    context.core_pipeline = ACMSPipeline(
        strategy_selector=ActorPhaseStrategySelector(),
    )


@given("the ACMS pipeline with a SpecBudgetAllocator")
def step_pipeline_with_spec_allocator(context: Context) -> None:
    context.core_pipeline = ACMSPipeline(
        budget_allocator=SpecBudgetAllocator(),
    )


@given("the ACMS pipeline with a RelevanceRecencyPriorityScorer")
def step_pipeline_with_rrp_scorer(context: Context) -> None:
    context.core_pipeline = ACMSPipeline(
        scorer=RelevanceRecencyPriorityScorer(),
    )


@given("the ACMS pipeline with a ConstrainedKnapsackPacker")
def step_pipeline_with_constrained_packer(context: Context) -> None:
    context.core_pipeline = ACMSPipeline(
        packer=ConstrainedKnapsackPacker(),
    )


@given("the ACMS pipeline with a PriorityCoherenceOrderer")
def step_pipeline_with_priority_orderer(context: Context) -> None:
    context.core_pipeline = ACMSPipeline(
        orderer=PriorityCoherenceOrderer(),
    )


# ---------------------------------------------------------------------------
# Pipeline Integration — When steps
# ---------------------------------------------------------------------------


@when("I assemble context through the core pipeline")
def step_assemble_core_pipeline(context: Context) -> None:
    budget = getattr(
        context, "core_budget", ContextBudget(max_tokens=100000, reserved_tokens=0)
    )
    payload = context.core_pipeline.assemble(
        plan_id=_TEST_PLAN_ID,
        fragments=context.core_fragments,
        budget=budget,
    )
    context.core_payload = payload


@when("I assemble context through the core pipeline with budget")
def step_assemble_core_pipeline_with_budget(context: Context) -> None:
    payload = context.core_pipeline.assemble(
        plan_id=_TEST_PLAN_ID,
        fragments=context.core_fragments,
        budget=context.core_budget,
    )
    context.core_payload = payload


# ---------------------------------------------------------------------------
# Pipeline Integration — Then steps
# ---------------------------------------------------------------------------


@then("the core pipeline output should contain {count:d} fragment")
def step_core_pipeline_count_singular(context: Context, count: int) -> None:
    assert len(context.core_payload.fragments) == count, (
        f"Expected {count}, got {len(context.core_payload.fragments)}"
    )


@then("the core pipeline output should contain {count:d} fragments")
def step_core_pipeline_count_plural(context: Context, count: int) -> None:
    assert len(context.core_payload.fragments) == count, (
        f"Expected {count}, got {len(context.core_payload.fragments)}"
    )


@then("the core pipeline output fragments should have updated relevance scores")
def step_core_pipeline_scorer_applied(context: Context) -> None:
    for frag in context.core_payload.fragments:
        assert "_original_relevance" in frag.metadata, (
            f"Fragment {frag.uko_node} missing _original_relevance metadata"
        )


@then("the core pipeline output total tokens should be at most {max_tokens:d}")
def step_core_pipeline_total_at_most(context: Context, max_tokens: int) -> None:
    assert context.core_payload.total_tokens <= max_tokens, (
        f"Expected <= {max_tokens}, got {context.core_payload.total_tokens}"
    )
