"""Step definitions for TDD Issue #1152 — budget eviction deletes instead of demotes.

These steps exercise ``ContextTierService`` to verify that hot-tier fragments
evicted by budget enforcement (``_enforce_hot_budget``) are **demoted to the
warm tier** rather than permanently deleted.

Bug #1152: ``_enforce_hot_budget()`` in ``TierRuntimeMixin`` does
``del self._hot[oldest_id]`` — the evicted fragment is destroyed.  The
specification's downward tier lifecycle (§Plan Lifecycle ACMS Actions:
"Hot context archived to warm") requires demotion, not deletion.

Similarly, ``evict_lru()`` in ``ContextTierService`` permanently deletes
fragments from the tier store without demoting them to the next lower tier.

The assertions in these steps will **fail** until the bug is fixed,
proving the bug exists.  The ``@tdd_expected_fail`` tag on the feature
inverts the result so CI passes.
"""

from __future__ import annotations

from datetime import UTC, datetime, timedelta

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

from cleveragents.application.services.context_tiers import ContextTierService
from cleveragents.domain.models.acms.tiers import (
    ContextTier,
    TierBudget,
    TieredFragment,
)

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


def _make_eviction_fragment(
    fragment_id: str,
    tier: ContextTier,
    token_count: int = 50,
    content: str = "test content",
    last_accessed: datetime | None = None,
) -> TieredFragment:
    """Create a ``TieredFragment`` for eviction tests."""
    fragment: TieredFragment = TieredFragment(
        fragment_id=fragment_id,
        content=content,
        tier=tier,
        token_count=token_count,
        project_name="test-project",
    )
    if last_accessed is not None:
        fragment.last_accessed = last_accessed
    return fragment


# ---------------------------------------------------------------------------
# Scenario 1: Budget-evicted fragment demoted to warm (not deleted)
# ---------------------------------------------------------------------------


@given(
    "a context tier service with a hot tier budget of {n:d} tokens for eviction test"
)
def step_eviction_service_small_budget(context: Context, n: int) -> None:
    """Create a ContextTierService with a constrained hot tier budget."""
    service: ContextTierService = ContextTierService(settings=None)
    service._budget = TierBudget(
        max_tokens_hot=n,
        max_decisions_warm=500,
        max_decisions_cold=5000,
    )
    context.eviction_service = service
    context.eviction_budget_tokens = n


@given(
    "the hot tier contains two 50-token fragments filling the budget for eviction test"
)
def step_eviction_fill_hot_tier(context: Context) -> None:
    """Fill the hot tier with exactly two 50-token fragments (100 tokens)."""
    service: ContextTierService = context.eviction_service

    # Fragment A: oldest (lower last_accessed)
    frag_a: TieredFragment = _make_eviction_fragment(
        fragment_id="frag-evict-oldest",
        tier=ContextTier.HOT,
        token_count=50,
        content="oldest fragment content",
        last_accessed=datetime.now(tz=UTC) - timedelta(minutes=10),
    )
    service.store(frag_a)

    # Fragment B: newer
    frag_b: TieredFragment = _make_eviction_fragment(
        fragment_id="frag-evict-newer",
        tier=ContextTier.HOT,
        token_count=50,
        content="newer fragment content",
        last_accessed=datetime.now(tz=UTC) - timedelta(minutes=5),
    )
    service.store(frag_b)

    context.eviction_oldest_id = "frag-evict-oldest"
    context.eviction_newer_id = "frag-evict-newer"

    metrics = service.get_metrics()
    assert metrics.hot_count == 2, (
        f"Expected 2 fragments in hot tier, got {metrics.hot_count}"
    )


@when("I store a third 50-token fragment in the hot tier triggering budget eviction")
def step_eviction_store_overflow(context: Context) -> None:
    """Store a fragment that exceeds the hot tier budget, triggering eviction."""
    service: ContextTierService = context.eviction_service
    overflow_frag: TieredFragment = _make_eviction_fragment(
        fragment_id="frag-evict-overflow",
        tier=ContextTier.HOT,
        token_count=50,
        content="overflow fragment content",
    )
    service.store(overflow_frag)


@then("the evicted fragment should exist in the warm tier not be permanently deleted")
def step_eviction_check_warm_tier(context: Context) -> None:
    """Assert the budget-evicted fragment was demoted to warm, not deleted.

    Bug #1152: ``_enforce_hot_budget()`` does ``del self._hot[oldest_id]``
    which permanently destroys the fragment.  The correct behaviour is to
    demote the fragment to the warm tier, preserving it in the lower tier
    consistent with the specification's downward lifecycle flow.
    """
    service: ContextTierService = context.eviction_service
    oldest_id: str = context.eviction_oldest_id

    # The evicted fragment should NOT be in the hot tier
    assert oldest_id not in service._hot, (
        f"Fragment '{oldest_id}' is still in the hot tier after budget "
        f"eviction — budget enforcement did not evict it."
    )

    # The evicted fragment SHOULD be in the warm tier (demoted, not deleted)
    assert oldest_id in service._warm, (
        f"Fragment '{oldest_id}' was evicted from the hot tier but is NOT "
        f"in the warm tier.  _enforce_hot_budget() permanently deletes "
        f"evicted fragments via 'del self._hot[oldest_id]' instead of "
        f"demoting them to the warm tier (bug #1152).  The specification "
        f"(§Plan Lifecycle ACMS Actions) describes a downward lifecycle: "
        f"'Hot context archived to warm.'  Evicted fragments should be "
        f"demoted to warm, not destroyed."
    )

    # Verify the fragment's tier metadata is updated to WARM
    demoted: TieredFragment = service._warm[oldest_id]
    assert demoted.tier == ContextTier.WARM, (
        f"Fragment '{oldest_id}' is in the warm store but its tier "
        f"metadata is {demoted.tier}, expected WARM."
    )


# ---------------------------------------------------------------------------
# Scenario 2: evict_lru also demotes to warm instead of deleting
# ---------------------------------------------------------------------------


@when("I call evict_lru on the hot tier to remove one fragment")
def step_eviction_call_evict_lru(context: Context) -> None:
    """Call evict_lru(HOT, 1) to evict the oldest hot-tier fragment."""
    service: ContextTierService = context.eviction_service
    evicted_ids: list[str] = service.evict_lru(ContextTier.HOT, count=1)
    context.eviction_lru_evicted_ids = evicted_ids


@then(
    "the LRU-evicted fragment should exist in the warm tier not be permanently deleted"
)
def step_eviction_lru_check_warm_tier(context: Context) -> None:
    """Assert the LRU-evicted fragment was demoted to warm, not deleted.

    Bug #1152: ``evict_lru()`` does ``del store[fid]`` which permanently
    destroys the fragment.  The correct behaviour is to demote the
    fragment to the next lower tier (hot → warm), preserving it
    consistent with the specification's downward lifecycle flow.
    """
    service: ContextTierService = context.eviction_service
    evicted_ids: list[str] = context.eviction_lru_evicted_ids

    assert len(evicted_ids) == 1, f"Expected 1 evicted fragment, got {len(evicted_ids)}"

    evicted_id: str = evicted_ids[0]

    # The evicted fragment should be the oldest one
    assert evicted_id == context.eviction_oldest_id, (
        f"Expected oldest fragment '{context.eviction_oldest_id}' to be "
        f"evicted, but '{evicted_id}' was evicted instead."
    )

    # The evicted fragment should NOT be in the hot tier
    assert evicted_id not in service._hot, (
        f"Fragment '{evicted_id}' is still in the hot tier after eviction."
    )

    # The evicted fragment SHOULD be in the warm tier (demoted, not deleted)
    assert evicted_id in service._warm, (
        f"Fragment '{evicted_id}' was evicted from the hot tier but is NOT "
        f"in the warm tier.  evict_lru() permanently deletes evicted "
        f"fragments via 'del store[fid]' instead of demoting them to the "
        f"warm tier (bug #1152).  The specification (§Plan Lifecycle ACMS "
        f"Actions) describes a downward lifecycle: 'Hot context archived "
        f"to warm.'  Evicted fragments should be demoted to warm, not "
        f"destroyed."
    )

    # Verify the fragment's tier metadata is updated to WARM
    demoted: TieredFragment = service._warm[evicted_id]
    assert demoted.tier == ContextTier.WARM, (
        f"Fragment '{evicted_id}' is in the warm store but its tier "
        f"metadata is {demoted.tier}, expected WARM."
    )
