"""Step definitions for acms_advanced_strategies_coverage_boost.feature.

Targets uncovered lines in acms_advanced_strategies.py:
  - Line  97 : ArceStrategy.max_iterations property
  - Line 158 : ARCE refinement loop non-convergence path (prev_total update)
  - Line 208 : _refine_scores empty-fragments early return
  - Lines 230-231: _refine_scores prefix-boost branch
  - Lines 267-269: TemporalArchaeologyStrategy.capabilities
  - Lines 364-365: PlanDecisionContextStrategy.capabilities
"""

from __future__ import annotations

from typing import Any

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

from cleveragents.application.services.acms_advanced_strategies import (
    ArceStrategy,
    PlanDecisionContextStrategy,
    TemporalArchaeologyStrategy,
)
from cleveragents.domain.models.core.context_fragment import (
    ContextBudget,
    ContextFragment,
    FragmentProvenance,
)

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

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


def _frag(
    content: str = "hello world",
    *,
    fragment_id: str | None = None,
    relevance_score: float = 0.5,
    detail_depth: int = 3,
    token_count: int = 10,
    tier: str = "warm",
    uko_node: str = "test://default/node",
) -> ContextFragment:
    """Create a ContextFragment with test defaults."""
    kwargs: dict[str, Any] = {
        "content": content,
        "relevance_score": relevance_score,
        "detail_depth": detail_depth,
        "token_count": token_count,
        "tier": tier,
        "uko_node": uko_node,
        "provenance": _DEFAULT_PROVENANCE,
    }
    if fragment_id is not None:
        kwargs["fragment_id"] = fragment_id
    return ContextFragment(**kwargs)


def _large_budget() -> ContextBudget:
    """Return a budget large enough that packing never truncates."""
    return ContextBudget(max_tokens=100_000, reserved_tokens=0)


# ===================================================================
# ArceStrategy — max_iterations property  (line 97)
# ===================================================================


@given("an ArceStrategy with max_iterations set to {n:d}")
def step_arce_with_max_iter(context: Context, n: int) -> None:
    context.strategy = ArceStrategy(max_iterations=n)


@given("an ArceStrategy with default parameters")
def step_arce_default(context: Context) -> None:
    context.strategy = ArceStrategy()


@then("the max_iterations property returns {n:d}")
def step_max_iterations_value(context: Context, n: int) -> None:
    assert context.strategy.max_iterations == n, (
        f"Expected max_iterations={n}, got {context.strategy.max_iterations}"
    )


# ===================================================================
# ARCE refinement loop — non-convergence path  (line 158)
# ===================================================================


@given(
    "an ArceStrategy with max_iterations set to {n:d} "
    "and convergence_threshold {thresh:g}"
)
def step_arce_custom(context: Context, n: int, thresh: float) -> None:
    context.strategy = ArceStrategy(
        max_iterations=n,
        convergence_threshold=thresh,
    )


@given(
    "a set of fragments with shared UKO prefixes "
    "that produce score changes each iteration"
)
def step_shared_prefix_fragments(context: Context) -> None:
    # We need enough fragments so that the anchor set and non-anchor set
    # share a UKO prefix.  The non-anchor fragments will get a 0.05 boost
    # each iteration, ensuring improvement > 0 (threshold is 0.0 means
    # convergence is impossible, so the loop always runs to max_iterations).
    #
    # Fragment A — high relevance, anchor candidate
    # Fragments B, C — same UKO prefix as A, lower relevance -> boosted
    # Fragment D — different prefix, no boost
    context.fragments = [
        _frag(
            content="anchor content for domain",
            fragment_id="frag-A",
            relevance_score=0.9,
            detail_depth=5,
            uko_node="uko://domain/module/a",
        ),
        _frag(
            content="related content for domain module",
            fragment_id="frag-B",
            relevance_score=0.3,
            detail_depth=2,
            uko_node="uko://domain/module/b",
        ),
        _frag(
            content="another related domain module item",
            fragment_id="frag-C",
            relevance_score=0.2,
            detail_depth=1,
            uko_node="uko://domain/module/c",
        ),
        _frag(
            content="unrelated item in other prefix",
            fragment_id="frag-D",
            relevance_score=0.4,
            detail_depth=4,
            uko_node="uko://other/section/d",
        ),
    ]


@when("the ArceStrategy assembles the fragments with a large budget")
def step_arce_assemble(context: Context) -> None:
    context.result = context.strategy.assemble(
        context.fragments,
        _large_budget(),
    )


@then("the result contains fragments ordered by refined score")
def step_result_ordered(context: Context) -> None:
    result = context.result
    assert len(result) == len(context.fragments), (
        f"Expected {len(context.fragments)} fragments, got {len(result)}"
    )
    # Just verify we got a non-empty ordering — the detailed score
    # verification is done in the prefix-boost scenario below.
    assert len(result) > 0


# ===================================================================
# _refine_scores — empty fragments guard  (line 208)
# ===================================================================


@when("_refine_scores is called with an empty fragment list and some scores")
def step_refine_empty(context: Context) -> None:
    original_scores = {"phantom-id": 0.42}
    context.refine_result = context.strategy._refine_scores([], original_scores)
    context.original_scores = original_scores


@then("the original scores dict is returned unchanged")
def step_refine_unchanged(context: Context) -> None:
    assert context.refine_result == context.original_scores


# ===================================================================
# _refine_scores — prefix-boost branch  (lines 230-231)
# ===================================================================


@given("four fragments where two share UKO prefix with the anchor fragment")
def step_four_fragments(context: Context) -> None:
    # With 4 fragments the anchor set is max(1, 4*3//10) = 1.
    # Only the highest-scored fragment becomes the anchor.
    # Fragments sharing its UKO prefix but NOT in the anchor set get boosted.
    context.boost_fragments = [
        _frag(
            content="anchor content item",
            fragment_id="anchor-1",
            relevance_score=0.9,
            detail_depth=5,
            uko_node="uko://shared/prefix/a",
        ),
        _frag(
            content="same prefix lower score",
            fragment_id="related-2",
            relevance_score=0.3,
            detail_depth=2,
            uko_node="uko://shared/prefix/b",
        ),
        _frag(
            content="same prefix even lower",
            fragment_id="related-3",
            relevance_score=0.2,
            detail_depth=1,
            uko_node="uko://shared/prefix/c",
        ),
        _frag(
            content="different prefix item",
            fragment_id="other-4",
            relevance_score=0.4,
            detail_depth=4,
            uko_node="uko://different/prefix/d",
        ),
    ]
    # Pre-compute initial scores (same algorithm as _initial_score)
    context.boost_scores: dict[str, float] = {}
    for frag in context.boost_fragments:
        depth_norm = frag.detail_depth / 9.0
        words = set(frag.content.lower().split())
        diversity = min(len(words) / max(frag.token_count, 1), 1.0)
        score = frag.relevance_score * 0.4 + depth_norm * 0.3 + diversity * 0.3
        context.boost_scores[frag.fragment_id] = score


@when("_refine_scores is called with those fragments and initial scores")
def step_refine_with_fragments(context: Context) -> None:
    context.refined_scores = context.strategy._refine_scores(
        context.boost_fragments,
        context.boost_scores,
    )


@then("non-anchor fragments sharing the anchor prefix receive a 0.05 boost")
def step_verify_boost(context: Context) -> None:
    original = context.boost_scores
    refined = context.refined_scores

    # anchor-1 has the highest score and is in the anchor set — no boost
    assert refined["anchor-1"] == original["anchor-1"], (
        "Anchor fragment should NOT be boosted"
    )

    # related-2 and related-3 share the anchor prefix and are NOT anchors
    # so they should be boosted by 0.05
    for fid in ("related-2", "related-3"):
        expected = min(original[fid] + 0.05, 1.0)
        assert abs(refined[fid] - expected) < 1e-9, (
            f"{fid}: expected {expected}, got {refined[fid]}"
        )

    # other-4 has a different prefix — no boost
    assert refined["other-4"] == original["other-4"], (
        "Fragment with a different prefix should NOT be boosted"
    )


# ===================================================================
# TemporalArchaeologyStrategy.capabilities  (lines 267-269)
# ===================================================================


@given("a TemporalArchaeologyStrategy instance")
def step_temporal_instance(context: Context) -> None:
    context.strategy = TemporalArchaeologyStrategy()


@then("its capabilities have supports_temporal_archaeology true")
def step_cap_temporal(context: Context) -> None:
    assert context.strategy.capabilities.supports_temporal_archaeology is True


@then("its capabilities have supports_graph_navigation true")
def step_cap_graph_true(context: Context) -> None:
    assert context.strategy.capabilities.supports_graph_navigation is True


@then("its capabilities have supports_semantic_search false")
def step_cap_semantic_false(context: Context) -> None:
    assert context.strategy.capabilities.supports_semantic_search is False


# ===================================================================
# PlanDecisionContextStrategy.capabilities  (lines 364-365)
# ===================================================================


@given("a PlanDecisionContextStrategy instance")
def step_plan_decision_instance(context: Context) -> None:
    context.strategy = PlanDecisionContextStrategy()


@then("its capabilities have supports_graph_navigation false")
def step_cap_graph_false(context: Context) -> None:
    assert context.strategy.capabilities.supports_graph_navigation is False
