"""Step definitions for execute_phase_context_assembler_coverage.feature.

Exercises all uncovered lines in
``cleveragents.application.services.execute_phase_context_assembler``:
  - ExecutePhaseContextAssembler protocol stub (line 36)
  - _resolve_execute_view success and exception paths (lines 57-66)
  - _path_matches static method (lines 71-75)
  - _resource_matches static method (lines 83-89)
  - _to_context_fragment conversion (lines 99-117)
  - assemble() method: filtering, logging, pipeline call (lines 122-240)
"""

from __future__ import annotations

from typing import Any
from unittest.mock import MagicMock, patch

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

from cleveragents.application.services.execute_phase_context_assembler import (
    ACMSExecutePhaseContextAssembler,
)
from cleveragents.domain.models.acms.crp import AssembledContext
from cleveragents.domain.models.acms.tiers import TieredFragment
from cleveragents.domain.models.core.context_policy import (
    ContextView,
    ProjectContextPolicy,
)

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


def _make_tiered_fragment(
    *,
    fragment_id: str = "frag-001",
    content: str = "hello world",
    resource_id: str = "res:test",
    project_name: str = "proj-alpha",
    token_count: int = 5,
    metadata: dict[str, Any] | None = None,
) -> TieredFragment:
    """Build a TieredFragment for testing."""
    return TieredFragment(
        fragment_id=fragment_id,
        content=content,
        resource_id=resource_id,
        project_name=project_name,
        token_count=token_count,
        metadata=metadata if metadata is not None else {},
    )


def _make_plan(
    *,
    plan_id: str = "01ARZ3NDEKTSV4RRFFQ69G5FAV",
    project_names: list[str] | None = None,
) -> MagicMock:
    """Build a mock plan with project_links."""
    plan = MagicMock()
    plan.identity.plan_id = plan_id
    if project_names is None:
        plan.project_links = []
    else:
        links = []
        for name in project_names:
            link = MagicMock()
            link.project_name = name
            links.append(link)
        plan.project_links = links
    return plan


def _make_pipeline_result() -> MagicMock:
    """Build a standard mock pipeline result."""
    pr = MagicMock()
    pr.fragments = ()
    pr.total_tokens = 5
    pr.budget_used = 0.1
    pr.strategies_used = ("relevance",)
    pr.context_hash = "hash123"
    pr.preamble = None
    pr.provenance_map = {}
    return pr


def _make_assembler(
    *,
    policy: ProjectContextPolicy | None = None,
    policy_exception: bool = False,
    scoped_fragments: list[TieredFragment] | None = None,
    pipeline_result: MagicMock | None = None,
) -> ACMSExecutePhaseContextAssembler:
    """Build an ACMSExecutePhaseContextAssembler with mocked dependencies."""
    tier_service = MagicMock()
    if scoped_fragments is not None:
        tier_service.get_scoped_view.return_value = scoped_fragments
    else:
        tier_service.get_scoped_view.return_value = []

    repo = MagicMock()
    if policy_exception:
        repo.get_context_policy.side_effect = RuntimeError("db error")
    elif policy is not None:
        repo.get_context_policy.return_value = policy
    else:
        repo.get_context_policy.return_value = ProjectContextPolicy()

    mock_pipeline = MagicMock()
    if pipeline_result is not None:
        mock_pipeline.assemble.return_value = pipeline_result
    else:
        mock_pipeline.assemble.return_value = _make_pipeline_result()

    assembler = ACMSExecutePhaseContextAssembler(
        context_tier_service=tier_service,
        project_repository=repo,
        acms_pipeline=mock_pipeline,
        hot_max_tokens=4096,
    )
    return assembler


# ---------------------------------------------------------------------------
# Protocol (line 36)
# ---------------------------------------------------------------------------


@given("epcov a class implementing the ExecutePhaseContextAssembler protocol")
def step_epcov_protocol_stub(context: Context) -> None:

    class Stub:
        def assemble(self, plan: Any) -> AssembledContext | None:
            return None

    context.epcov_protocol_stub = Stub()


@when("epcov I call assemble on the protocol stub")
def step_epcov_call_protocol_assemble(context: Context) -> None:
    context.epcov_protocol_result = context.epcov_protocol_stub.assemble(None)


@then("epcov the result should be None")
def step_epcov_protocol_result_none(context: Context) -> None:
    assert context.epcov_protocol_result is None


# ---------------------------------------------------------------------------
# _resolve_execute_view (lines 57-66)
# ---------------------------------------------------------------------------


@given("epcov an assembler with a project repository returning a custom policy")
def step_epcov_assembler_custom_policy(context: Context) -> None:
    custom_view = ContextView(include_resources=["custom:*"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(policy=policy)


@when('epcov I resolve the execute view for "{project_name}"')
def step_epcov_resolve_execute_view(context: Context, project_name: str) -> None:
    context.epcov_resolved_view = context.epcov_assembler._resolve_execute_view(
        project_name
    )


@then("epcov the resolved view should have the custom include_resources")
def step_epcov_resolved_view_custom(context: Context) -> None:
    view = context.epcov_resolved_view
    assert view.include_resources == ["custom:*"], (
        f"Expected ['custom:*'], got {view.include_resources}"
    )


@given("epcov an assembler with a project repository that raises an exception")
def step_epcov_assembler_exception_policy(context: Context) -> None:
    context.epcov_assembler = _make_assembler(policy_exception=True)


@then("epcov the resolved view should be the default empty ContextView")
def step_epcov_resolved_view_default(context: Context) -> None:
    view = context.epcov_resolved_view
    assert view.include_resources == [], (
        f"Expected empty include_resources, got {view.include_resources}"
    )
    assert view.exclude_resources == [], (
        f"Expected empty exclude_resources, got {view.exclude_resources}"
    )


# ---------------------------------------------------------------------------
# _path_matches (lines 71-75)
# ---------------------------------------------------------------------------


@when('epcov I check path matching for "{path}" with no include or exclude')
def step_epcov_path_matches_no_rules(context: Context, path: str) -> None:
    context.epcov_path_result = ACMSExecutePhaseContextAssembler._path_matches(
        path, [], []
    )


@when('epcov I check path matching for "{path}" with include "{pattern}"')
def step_epcov_path_matches_include(context: Context, path: str, pattern: str) -> None:
    context.epcov_path_result = ACMSExecutePhaseContextAssembler._path_matches(
        path, [pattern], []
    )


@when('epcov I check path matching for "{path}" with exclude "{pattern}"')
def step_epcov_path_matches_exclude(context: Context, path: str, pattern: str) -> None:
    context.epcov_path_result = ACMSExecutePhaseContextAssembler._path_matches(
        path, [], [pattern]
    )


@then("epcov the path should match")
def step_epcov_path_should_match(context: Context) -> None:
    assert context.epcov_path_result is True, "Expected path to match"


@then("epcov the path should not match")
def step_epcov_path_should_not_match(context: Context) -> None:
    assert context.epcov_path_result is False, "Expected path to not match"


# ---------------------------------------------------------------------------
# _resource_matches (lines 83-89)
# ---------------------------------------------------------------------------


@when('epcov I check resource matching for "{rid}" with no include or exclude')
def step_epcov_resource_matches_no_rules(context: Context, rid: str) -> None:
    context.epcov_resource_result = ACMSExecutePhaseContextAssembler._resource_matches(
        rid, [], []
    )


@when('epcov I check resource matching for "{rid}" with include "{pattern}"')
def step_epcov_resource_matches_include(
    context: Context, rid: str, pattern: str
) -> None:
    context.epcov_resource_result = ACMSExecutePhaseContextAssembler._resource_matches(
        rid, [pattern], []
    )


@when('epcov I check resource matching for "{rid}" with exclude "{pattern}"')
def step_epcov_resource_matches_exclude(
    context: Context, rid: str, pattern: str
) -> None:
    context.epcov_resource_result = ACMSExecutePhaseContextAssembler._resource_matches(
        rid, [], [pattern]
    )


@then("epcov the resource should match")
def step_epcov_resource_should_match(context: Context) -> None:
    assert context.epcov_resource_result is True, "Expected resource to match"


@then("epcov the resource should not match")
def step_epcov_resource_should_not_match(context: Context) -> None:
    assert context.epcov_resource_result is False, "Expected resource to not match"


# ---------------------------------------------------------------------------
# _to_context_fragment (lines 99-117)
# ---------------------------------------------------------------------------


@given("epcov an assembler instance")
def step_epcov_assembler_instance(context: Context) -> None:
    context.epcov_assembler = _make_assembler()


@when("epcov I convert a tiered fragment with detail_depth 3 and relevance 0.8")
def step_epcov_convert_fragment_full(context: Context) -> None:
    """Test with int/float metadata values.

    CoreContextFragment.metadata requires dict[str, str], so non-string
    values will fail validation. We patch CoreContextFragment to accept
    arbitrary kwargs, exercising lines 99-117 including the isinstance
    checks on lines 100-104.
    """
    import cleveragents.application.services.execute_phase_context_assembler as asm_mod

    class MockCoreFragment:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    patcher = patch.object(asm_mod, "CoreContextFragment", MockCoreFragment)
    patcher.start()
    context.add_cleanup(patcher.stop)

    frag = _make_tiered_fragment(
        metadata={"detail_depth": 3, "relevance_score": 0.8, "path": "src/main.py"}
    )
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then("epcov the core fragment should have detail_depth 3 and relevance 0.8")
def step_epcov_core_frag_full(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.detail_depth == 3, f"Expected detail_depth 3, got {frag.detail_depth}"
    assert frag.relevance_score == 0.8, (
        f"Expected relevance 0.8, got {frag.relevance_score}"
    )


@when("epcov I convert a tiered fragment with no detail_depth or relevance")
def step_epcov_convert_fragment_defaults(context: Context) -> None:
    """Test with string-only metadata (no detail_depth or relevance keys)."""
    frag = _make_tiered_fragment(metadata={"path": "src/test.py"})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then("epcov the core fragment should have detail_depth 1 and relevance 0.5")
def step_epcov_core_frag_defaults(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.detail_depth == 1, f"Expected detail_depth 1, got {frag.detail_depth}"
    assert frag.relevance_score == 0.5, (
        f"Expected relevance 0.5, got {frag.relevance_score}"
    )


@when('epcov I convert a tiered fragment with detail_depth "high" and relevance "bad"')
def step_epcov_convert_fragment_non_numeric(context: Context) -> None:
    """detail_depth='high' and relevance_score='bad' are strings, so they
    fall through to defaults (line 101 else, line 103 else)."""
    frag = _make_tiered_fragment(
        metadata={"detail_depth": "high", "relevance_score": "bad", "path": "x.py"}
    )
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@when("epcov I convert a tiered fragment with relevance 1.5")
def step_epcov_convert_fragment_high_relevance(context: Context) -> None:
    """Relevance 1.5 exercises the clamping at line 104: min(1.0, max(0.0, 1.5))."""
    import cleveragents.application.services.execute_phase_context_assembler as asm_mod

    class MockCoreFragment:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    patcher = patch.object(asm_mod, "CoreContextFragment", MockCoreFragment)
    patcher.start()
    context.add_cleanup(patcher.stop)

    frag = _make_tiered_fragment(metadata={"relevance_score": 1.5, "path": "a.py"})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then("epcov the core fragment should have clamped relevance 1.0")
def step_epcov_core_frag_clamped_high(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.relevance_score == 1.0, (
        f"Expected clamped relevance 1.0, got {frag.relevance_score}"
    )


@when("epcov I convert a tiered fragment with relevance -0.3")
def step_epcov_convert_fragment_neg_relevance(context: Context) -> None:
    """Relevance -0.3 exercises clamping at line 104: min(1.0, max(0.0, -0.3))."""
    import cleveragents.application.services.execute_phase_context_assembler as asm_mod

    class MockCoreFragment:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    patcher = patch.object(asm_mod, "CoreContextFragment", MockCoreFragment)
    patcher.start()
    context.add_cleanup(patcher.stop)

    frag = _make_tiered_fragment(metadata={"relevance_score": -0.3, "path": "a.py"})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then("epcov the core fragment should have clamped relevance 0.0")
def step_epcov_core_frag_clamped_low(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.relevance_score == 0.0, (
        f"Expected clamped relevance 0.0, got {frag.relevance_score}"
    )


@when("epcov I convert a tiered fragment with empty resource_id")
def step_epcov_convert_fragment_no_resource_id(context: Context) -> None:
    """Empty resource_id triggers the 'uko:' fallback on line 107."""
    import cleveragents.application.services.execute_phase_context_assembler as asm_mod

    class MockCoreFragment:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    patcher = patch.object(asm_mod, "CoreContextFragment", MockCoreFragment)
    patcher.start()
    context.add_cleanup(patcher.stop)

    frag = _make_tiered_fragment(resource_id="", metadata={"path": "a.py"})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then('epcov the core fragment uko_node should start with "uko:"')
def step_epcov_core_frag_uko_fallback(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.uko_node.startswith("uko:"), (
        f"Expected uko_node starting with 'uko:', got {frag.uko_node}"
    )
    assert frag.provenance.resource_uri == "unknown", (
        f"Expected provenance resource_uri 'unknown', got {frag.provenance.resource_uri}"
    )


@when('epcov I convert a tiered fragment with path "src/main.py" in metadata')
def step_epcov_convert_fragment_with_path(context: Context) -> None:
    frag = _make_tiered_fragment(metadata={"path": "src/main.py"})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then('epcov the core fragment provenance location should be "src/main.py"')
def step_epcov_core_frag_provenance_path(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.provenance.location == "src/main.py", (
        f"Expected 'src/main.py', got {frag.provenance.location}"
    )


@when("epcov I convert a tiered fragment with non-string path in metadata")
def step_epcov_convert_fragment_nonstring_path(context: Context) -> None:
    """Non-string path triggers the isinstance check on line 114."""
    import cleveragents.application.services.execute_phase_context_assembler as asm_mod

    class MockCoreFragment:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    class MockProvenance:
        def __init__(self, **kwargs: Any) -> None:
            for k, v in kwargs.items():
                setattr(self, k, v)

    patcher_cf = patch.object(asm_mod, "CoreContextFragment", MockCoreFragment)
    patcher_prov = patch.object(asm_mod, "CoreFragmentProvenance", MockProvenance)
    patcher_cf.start()
    patcher_prov.start()
    context.add_cleanup(patcher_cf.stop)
    context.add_cleanup(patcher_prov.stop)

    frag = _make_tiered_fragment(metadata={"path": 12345})
    context.epcov_core_frag = context.epcov_assembler._to_context_fragment(
        frag, strategy="test_strategy"
    )


@then("epcov the core fragment provenance location should be empty")
def step_epcov_core_frag_provenance_empty(context: Context) -> None:
    frag = context.epcov_core_frag
    assert frag.provenance.location == "", (
        f"Expected empty location, got {frag.provenance.location}"
    )


# ---------------------------------------------------------------------------
# assemble: no project links (lines 122-126)
# ---------------------------------------------------------------------------


@given("epcov a plan with no project links")
def step_epcov_plan_no_links(context: Context) -> None:
    context.epcov_plan = _make_plan(project_names=None)


@when("epcov I call assemble on the plan")
def step_epcov_call_assemble(context: Context) -> None:
    context.epcov_error = None
    try:
        context.epcov_assembled = context.epcov_assembler.assemble(context.epcov_plan)
    except Exception as exc:
        context.epcov_error = exc
        context.epcov_assembled = None


@then("epcov the assembled result should be None")
def step_epcov_assembled_none(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    assert context.epcov_assembled is None, (
        f"Expected None, got {context.epcov_assembled}"
    )


# ---------------------------------------------------------------------------
# assemble: empty scoped view (lines 128-131)
# ---------------------------------------------------------------------------


@given("epcov an assembler with scoped view returning empty list")
def step_epcov_assembler_empty_scoped(context: Context) -> None:
    context.epcov_assembler = _make_assembler(scoped_fragments=[])


@given("epcov a plan with project links")
def step_epcov_plan_with_links(context: Context) -> None:
    context.epcov_plan = _make_plan(project_names=["proj-alpha"])


# ---------------------------------------------------------------------------
# assemble: fragment excluded by missing view (lines 140-144)
# ---------------------------------------------------------------------------


@given('epcov an assembler with views only for "proj-alpha"')
def step_epcov_assembler_views_only_alpha(context: Context) -> None:
    """Fragments with project_name not in plan project_links get view=None."""
    frag_alpha = _make_tiered_fragment(
        fragment_id="frag-alpha",
        project_name="proj-alpha",
        resource_id="res:alpha",
        content="alpha content",
        metadata={"path": "src/alpha.py"},
    )
    frag_orphan = _make_tiered_fragment(
        fragment_id="frag-orphan",
        project_name="proj-unknown",
        resource_id="res:orphan",
        content="orphan content",
        metadata={"path": "src/orphan.py"},
    )
    context.epcov_assembler = _make_assembler(
        scoped_fragments=[frag_alpha, frag_orphan],
        pipeline_result=_make_pipeline_result(),
    )


@given('epcov a plan with project links for "proj-alpha" and "proj-beta"')
def step_epcov_plan_alpha_beta(context: Context) -> None:
    context.epcov_plan = _make_plan(project_names=["proj-alpha", "proj-beta"])


@given('epcov scoped fragments for "proj-alpha" and "proj-beta"')
def step_epcov_scoped_fragments_alpha_beta(context: Context) -> None:
    pass  # Already set in assembler setup


@then("epcov the assembled result should include only proj-alpha fragments")
def step_epcov_assembled_only_alpha(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].uko_node == "res:alpha"


# ---------------------------------------------------------------------------
# assemble: resource filtering (lines 146-153)
# ---------------------------------------------------------------------------


@given('epcov an assembler with resource include rule "allowed:*"')
def step_epcov_assembler_resource_include(context: Context) -> None:
    custom_view = ContextView(include_resources=["allowed:*"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(
        policy=policy, pipeline_result=_make_pipeline_result()
    )


@given('epcov scoped fragments with resource ids "allowed:file" and "denied:file"')
def step_epcov_scoped_frags_resource_filter(context: Context) -> None:
    frag_allowed = _make_tiered_fragment(
        fragment_id="frag-allowed",
        resource_id="allowed:file",
        content="allowed content",
        metadata={"path": "src/allowed.py"},
    )
    frag_denied = _make_tiered_fragment(
        fragment_id="frag-denied",
        resource_id="denied:file",
        content="denied content",
        metadata={"path": "src/denied.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [
        frag_allowed,
        frag_denied,
    ]


@then("epcov the assembled result should include only the allowed resource fragment")
def step_epcov_assembled_only_allowed(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].uko_node == "allowed:file"


@given('epcov an assembler with resource exclude rule "secret:*"')
def step_epcov_assembler_resource_exclude(context: Context) -> None:
    custom_view = ContextView(exclude_resources=["secret:*"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(
        policy=policy, pipeline_result=_make_pipeline_result()
    )


@given('epcov scoped fragments with resource ids "public:file" and "secret:file"')
def step_epcov_scoped_frags_resource_exclude(context: Context) -> None:
    frag_public = _make_tiered_fragment(
        fragment_id="frag-public",
        resource_id="public:file",
        content="public content",
        metadata={"path": "src/public.py"},
    )
    frag_secret = _make_tiered_fragment(
        fragment_id="frag-secret",
        resource_id="secret:file",
        content="secret content",
        metadata={"path": "src/secret.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [
        frag_public,
        frag_secret,
    ]


@then("epcov the assembled result should include only the public resource fragment")
def step_epcov_assembled_only_public(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].uko_node == "public:file"


# ---------------------------------------------------------------------------
# assemble: path filtering (lines 155-170)
# ---------------------------------------------------------------------------


@given('epcov an assembler with path include rule "src/**"')
def step_epcov_assembler_path_include(context: Context) -> None:
    custom_view = ContextView(include_paths=["src/**"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(
        policy=policy, pipeline_result=_make_pipeline_result()
    )


@given('epcov scoped fragments with paths "src/main.py" and "docs/readme.md"')
def step_epcov_scoped_frags_path_filter(context: Context) -> None:
    frag_src = _make_tiered_fragment(
        fragment_id="frag-src",
        resource_id="res:src",
        content="source content",
        metadata={"path": "src/main.py"},
    )
    frag_docs = _make_tiered_fragment(
        fragment_id="frag-docs",
        resource_id="res:docs",
        content="docs content",
        metadata={"path": "docs/readme.md"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [
        frag_src,
        frag_docs,
    ]


@then("epcov the assembled result should include only the src fragment")
def step_epcov_assembled_only_src(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].provenance.location == "src/main.py"


@given('epcov an assembler with path exclude rule "vendor/**"')
def step_epcov_assembler_path_exclude(context: Context) -> None:
    custom_view = ContextView(exclude_paths=["vendor/**"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(
        policy=policy, pipeline_result=_make_pipeline_result()
    )


@given('epcov scoped fragments with paths "src/main.py" and "vendor/lib.py"')
def step_epcov_scoped_frags_path_exclude(context: Context) -> None:
    frag_src = _make_tiered_fragment(
        fragment_id="frag-src",
        resource_id="res:src",
        content="source content",
        metadata={"path": "src/main.py"},
    )
    frag_vendor = _make_tiered_fragment(
        fragment_id="frag-vendor",
        resource_id="res:vendor",
        content="vendor content",
        metadata={"path": "vendor/lib.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [
        frag_src,
        frag_vendor,
    ]


@then("epcov the assembled result should include only the non-vendor fragment")
def step_epcov_assembled_only_nonvendor(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].provenance.location == "src/main.py"


@given("epcov a scoped fragment with no path metadata")
def step_epcov_scoped_frag_no_path(context: Context) -> None:
    """Fragment with no path metadata and include_paths set → excluded (line 168-170)."""
    frag = _make_tiered_fragment(
        fragment_id="frag-nopath",
        resource_id="res:nopath",
        content="no path content",
        metadata={},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [frag]


# ---------------------------------------------------------------------------
# assemble: max_file_size filtering (lines 172-175)
# ---------------------------------------------------------------------------


@given("epcov an assembler with max_file_size of 10 bytes")
def step_epcov_assembler_max_file_size(context: Context) -> None:
    custom_view = ContextView(max_file_size=10)
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(policy=policy)


@given("epcov a scoped fragment with content exceeding 10 bytes")
def step_epcov_scoped_frag_big_content(context: Context) -> None:
    frag = _make_tiered_fragment(
        fragment_id="frag-big",
        resource_id="res:big",
        content="this content is definitely more than 10 bytes long",
        metadata={"path": "src/big.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [frag]


# ---------------------------------------------------------------------------
# assemble: max_total_size filtering (lines 177-184)
# ---------------------------------------------------------------------------


@given("epcov an assembler with max_total_size of 20 bytes")
def step_epcov_assembler_max_total_size(context: Context) -> None:
    custom_view = ContextView(max_total_size=20)
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(
        policy=policy, pipeline_result=_make_pipeline_result()
    )


@given("epcov two scoped fragments each with 15 byte content")
def step_epcov_scoped_frags_total_size(context: Context) -> None:
    frag1 = _make_tiered_fragment(
        fragment_id="frag-1",
        resource_id="res:1",
        content="0123456789abcde",  # 15 bytes
        metadata={"path": "src/a.py"},
    )
    frag2 = _make_tiered_fragment(
        fragment_id="frag-2",
        resource_id="res:2",
        content="fghijklmnopqrst",  # 15 bytes
        metadata={"path": "src/b.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [frag1, frag2]


@then("epcov the assembled result should include only the first fragment")
def step_epcov_assembled_only_first(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    pipeline = context.epcov_assembler._pipeline
    assert pipeline.assemble.called, "Pipeline assemble should have been called"
    call_kwargs = pipeline.assemble.call_args[1]
    frags = call_kwargs.get("fragments", [])
    assert len(frags) == 1, f"Expected 1 fragment, got {len(frags)}"
    assert frags[0].uko_node == "res:1"


# ---------------------------------------------------------------------------
# assemble: all filtered out (lines 193-203)
# ---------------------------------------------------------------------------


@given('epcov an assembler with resource include rule "nonexistent:*"')
def step_epcov_assembler_resource_nonexistent(context: Context) -> None:
    custom_view = ContextView(include_resources=["nonexistent:*"])
    policy = ProjectContextPolicy(execute_view=custom_view)
    context.epcov_assembler = _make_assembler(policy=policy)


@given('epcov scoped fragments with resource ids "other:a" and "other:b"')
def step_epcov_scoped_frags_all_excluded(context: Context) -> None:
    frag_a = _make_tiered_fragment(
        fragment_id="frag-a",
        resource_id="other:a",
        content="content a",
        metadata={"path": "src/a.py"},
    )
    frag_b = _make_tiered_fragment(
        fragment_id="frag-b",
        resource_id="other:b",
        content="content b",
        metadata={"path": "src/b.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [frag_a, frag_b]


# ---------------------------------------------------------------------------
# assemble: successful full pipeline (lines 186-240)
# ---------------------------------------------------------------------------


@given("epcov an assembler with default policy and pipeline")
def step_epcov_assembler_full_pipeline(context: Context) -> None:
    from cleveragents.domain.models.acms.crp import (
        ContextFragment as CRPFragment,
    )
    from cleveragents.domain.models.acms.crp import (
        FragmentProvenance as CRPProvenance,
    )

    mock_frag = CRPFragment(
        uko_node="res:good",
        content="good content here",
        detail_depth=1,
        token_count=10,
        relevance_score=0.5,
        provenance=CRPProvenance(
            resource_uri="res:good",
            location="src/good.py",
            strategy="execute_phase_context",
        ),
    )
    pr = _make_pipeline_result()
    pr.fragments = (mock_frag,)
    pr.total_tokens = 10
    pr.budget_used = 0.25
    pr.strategies_used = ("relevance",)
    pr.context_hash = "sha256hash"
    pr.preamble = "Context preamble"
    pr.provenance_map = {"frag-1": {"resource_uri": "res:test"}}
    context.epcov_assembler = _make_assembler(pipeline_result=pr)


@given("epcov scoped fragments that pass all filters")
def step_epcov_scoped_frags_pass_all(context: Context) -> None:
    frag = _make_tiered_fragment(
        fragment_id="frag-pass",
        resource_id="res:good",
        content="good content here",
        token_count=10,
        metadata={"path": "src/good.py"},
    )
    context.epcov_assembler._tier.get_scoped_view.return_value = [frag]


@then("epcov the assembled result should be an AssembledContext")
def step_epcov_assembled_is_context(context: Context) -> None:
    if context.epcov_error is not None:
        raise AssertionError(f"Unexpected error: {context.epcov_error}")
    result = context.epcov_assembled
    assert result is not None, "Expected non-None assembled result"
    assert isinstance(result, AssembledContext), (
        f"Expected AssembledContext, got {type(result)}"
    )


@then("epcov the assembled result should have fragments and metadata")
def step_epcov_assembled_has_data(context: Context) -> None:
    result = context.epcov_assembled
    assert result.total_tokens >= 0, "Expected non-negative total_tokens"
    assert result.budget_used >= 0.0, "Expected non-negative budget_used"
    assert result.context_hash, "Expected non-empty context_hash"
    assert result.strategies_used, "Expected non-empty strategies_used"
