"""Step definitions for scoped backend view filtering.

All steps prefixed with ``sv`` to avoid AmbiguousStep collisions with
existing step files in the project.
"""

from __future__ import annotations

from typing import Any

from behave import given, then, when  # type: ignore[import-untyped]

from cleveragents.domain.models.acms.scope_resolution import (
    ResourceAliasResolver,
    resolve_resource_scope,
    validate_project_scope,
    validate_resource_scope,
)
from cleveragents.domain.models.acms.scoped_view import (
    ResourceScope,
    ScopedBackendView,
    ScopeViolationError,
    create_scoped_backend_set,
)
from cleveragents.domain.models.acms.stubs import (
    InMemoryGraphBackend,
    InMemoryTextBackend,
    InMemoryVectorBackend,
)
from cleveragents.domain.models.acms.tiers import (
    ContextTier,
    TieredFragment,
)

__all__: list[str] = []


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


def _make_fragment(
    fid: str,
    project: str = "",
    resource: str = "",
    tier: str = "hot",
) -> TieredFragment:
    """Create a TieredFragment for testing."""
    return TieredFragment(
        fragment_id=fid,
        content=f"content-{fid}",
        tier=ContextTier(tier),
        project_name=project,
        resource_id=resource,
    )


def _make_project(
    name: str,
    namespace: str = "local",
    links: list[tuple[str, str | None]] | None = None,
) -> Any:
    """Create a NamespacedProject with linked resources."""
    from cleveragents.domain.models.core.project import (
        LinkedResource,
        NamespacedProject,
    )

    linked: list[LinkedResource] = []
    if links:
        for resource_id, alias in links:
            linked.append(LinkedResource(resource_id=resource_id, alias=alias))

    return NamespacedProject(
        name=name,
        namespace=namespace,
        linked_resources=linked,
    )


# ---------------------------------------------------------------------------
# ResourceScope
# ---------------------------------------------------------------------------


@given('sv a ResourceScope with resources "{res1}", "{res2}" and projects "{proj}"')
def step_sv_scope_two_res_one_proj(
    context: Any, res1: str, res2: str, proj: str
) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({res1, res2}),
        project_names=frozenset({proj}),
    )


@given('sv a ResourceScope with resources "{res1}" and projects "{proj}"')
def step_sv_scope_one_res_one_proj(context: Any, res1: str, proj: str) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({res1}),
        project_names=frozenset({proj}),
    )


@then('sv the scope should contain resource "{res}"')
def step_sv_scope_contains_res(context: Any, res: str) -> None:
    assert context.sv_scope.contains_resource(res), (
        f"Expected scope to contain resource '{res}'"
    )


@then('sv the scope should not contain resource "{res}"')
def step_sv_scope_not_contains_res(context: Any, res: str) -> None:
    assert not context.sv_scope.contains_resource(res), (
        f"Expected scope NOT to contain resource '{res}'"
    )


@then('sv the scope should contain project "{proj}"')
def step_sv_scope_contains_proj(context: Any, proj: str) -> None:
    assert context.sv_scope.contains_project(proj)


@then('sv the scope should not contain project "{proj}"')
def step_sv_scope_not_contains_proj(context: Any, proj: str) -> None:
    assert not context.sv_scope.contains_project(proj)


@then("sv modifying the scope should raise an error")
def step_sv_scope_frozen(context: Any) -> None:
    from pydantic import ValidationError

    try:
        context.sv_scope.resource_ids = frozenset({"NEW"})
        raise AssertionError("Expected frozen model error")
    except (ValidationError, AttributeError, TypeError):
        pass


@then('sv temporal_scope should be "{expected}"')
def step_sv_temporal_scope(context: Any, expected: str) -> None:
    assert context.sv_scope.temporal_scope == expected


@then('sv creating a scope with temporal_scope "{ts}" should raise ValidationError')
def step_sv_scope_bad_temporal(context: Any, ts: str) -> None:
    from pydantic import ValidationError

    try:
        ResourceScope(
            resource_ids=frozenset({"R1"}),
            project_names=frozenset({"p1"}),
            temporal_scope=ts,
        )
        raise AssertionError("Expected ValidationError")
    except ValidationError:
        pass


@then("sv creating a scope with no resources should succeed")
def step_sv_scope_empty_resources(context: Any) -> None:
    scope = ResourceScope(
        resource_ids=frozenset(),
        project_names=frozenset({"p1"}),
    )
    assert len(scope.resource_ids) == 0
    assert scope.contains_project("p1")


@then("sv creating a scope with no projects should raise ValidationError")
def step_sv_scope_no_projects(context: Any) -> None:
    from pydantic import ValidationError

    try:
        ResourceScope(
            resource_ids=frozenset({"R1"}),
            project_names=frozenset(),
        )
        raise AssertionError("Expected ValidationError")
    except ValidationError:
        pass


# ---------------------------------------------------------------------------
# ResourceScope path filtering
# ---------------------------------------------------------------------------


@given('sv a ResourceScope with include_paths "{p1}", "{p2}"')
def step_sv_scope_include_paths(context: Any, p1: str, p2: str) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({"R1"}),
        project_names=frozenset({"p1"}),
        include_paths=(p1, p2),
    )


@given('sv a ResourceScope with exclude_paths "{p1}", "{p2}"')
def step_sv_scope_exclude_paths(context: Any, p1: str, p2: str) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({"R1"}),
        project_names=frozenset({"p1"}),
        exclude_paths=(p1, p2),
    )


@given('sv a ResourceScope with include_paths "{inc}" and exclude_paths "{exc}"')
def step_sv_scope_inc_exc_paths(context: Any, inc: str, exc: str) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({"R1"}),
        project_names=frozenset({"p1"}),
        include_paths=(inc,),
        exclude_paths=(exc,),
    )


@then('sv path "{path}" should match')
def step_sv_path_matches(context: Any, path: str) -> None:
    assert context.sv_scope.matches_path(path), (
        f"Expected path '{path}' to match scope filters"
    )


@then('sv path "{path}" should not match')
def step_sv_path_not_matches(context: Any, path: str) -> None:
    assert not context.sv_scope.matches_path(path), (
        f"Expected path '{path}' NOT to match scope filters"
    )


# ---------------------------------------------------------------------------
# ScopedBackendView: fragment-level filtering
# ---------------------------------------------------------------------------


@given('sv a project-only ScopedBackendView for "{proj}"')
def step_sv_view_one_proj(context: Any, proj: str) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset({proj}),
    )
    context.sv_fragments = {}  # dict[str, TieredFragment]


@given('sv a project-and-resource ScopedBackendView for "{proj}" resources "{res}"')
def step_sv_view_proj_res(context: Any, proj: str, res: str) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset({proj}),
        resource_ids=frozenset({res}),
    )
    context.sv_fragments = {}


@given(
    'sv a deny-resource ScopedBackendView for "{proj}" allow "{r1}", "{r2}" deny "{d1}"'
)
def step_sv_view_proj_res_denied(
    context: Any, proj: str, r1: str, r2: str, d1: str
) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset({proj}),
        resource_ids=frozenset({r1, r2}),
        denied_resource_ids=frozenset({d1}),
    )
    context.sv_fragments = {}


@given('sv a fragment "{fid}" in project "{proj}" with resource "{res}"')
def step_sv_fragment_proj_res(context: Any, fid: str, proj: str, res: str) -> None:
    context.sv_fragments[fid] = _make_fragment(fid, project=proj, resource=res)


@given('sv a fragment "{fid}" with no project')
def step_sv_fragment_no_proj(context: Any, fid: str) -> None:
    context.sv_fragments[fid] = _make_fragment(fid, project="")


@then('sv fragment "{fid}" should be visible')
def step_sv_fragment_visible(context: Any, fid: str) -> None:
    frag = context.sv_fragments[fid]
    assert context.sv_view.is_visible(frag), f"Expected fragment '{fid}' to be visible"


@then('sv fragment "{fid}" should not be visible')
def step_sv_fragment_not_visible(context: Any, fid: str) -> None:
    frag = context.sv_fragments[fid]
    assert not context.sv_view.is_visible(frag), (
        f"Expected fragment '{fid}' NOT to be visible"
    )


# ---------------------------------------------------------------------------
# ScopedBackendView: backend search proxying
# ---------------------------------------------------------------------------


@given("sv a ScopedBackendView from that scope")
def step_sv_view_from_scope(context: Any) -> None:
    context.sv_view = ScopedBackendView.from_resource_scope(context.sv_scope)
    context.sv_fragments = {}


@given("sv an InMemoryTextBackend")
def step_sv_text_backend(context: Any) -> None:
    context.sv_text_backend = InMemoryTextBackend()


@given("sv an InMemoryVectorBackend")
def step_sv_vector_backend(context: Any) -> None:
    context.sv_vector_backend = InMemoryVectorBackend()


@given("sv an InMemoryGraphBackend")
def step_sv_graph_backend(context: Any) -> None:
    context.sv_graph_backend = InMemoryGraphBackend()


@when('sv I search text for "{query}"')
def step_sv_search_text(context: Any, query: str) -> None:
    context.sv_text_results = context.sv_view.search_text(
        context.sv_text_backend, query
    )
    context.sv_search_error = None


@then("sv the text search should return an empty list")
def step_sv_text_empty(context: Any) -> None:
    assert context.sv_text_results == []


@then("sv the text search should not raise an error")
def step_sv_text_no_error(context: Any) -> None:
    assert context.sv_search_error is None


@when("sv I search vector with embedding")
def step_sv_search_vector(context: Any) -> None:
    context.sv_vector_results = context.sv_view.search_vector(
        context.sv_vector_backend, [0.1, 0.2, 0.3]
    )


@then("sv the vector search should return an empty list")
def step_sv_vector_empty(context: Any) -> None:
    assert context.sv_vector_results == []


@when('sv I search graph for "{query}"')
def step_sv_search_graph(context: Any, query: str) -> None:
    context.sv_graph_result = context.sv_view.search_graph(
        context.sv_graph_backend, query
    )


@then("sv the graph search should return an empty GraphResult")
def step_sv_graph_empty(context: Any) -> None:
    assert context.sv_graph_result.triples == []


# ---------------------------------------------------------------------------
# effective_scope
# ---------------------------------------------------------------------------


@given(
    'sv a triple-resource ScopedBackendView for "{proj}" with "{r1}", "{r2}", "{r3}" denying "{d}"'
)
def step_sv_view_three_res_denied(
    context: Any, proj: str, r1: str, r2: str, r3: str, d: str
) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset({proj}),
        resource_ids=frozenset({r1, r2, r3}),
        denied_resource_ids=frozenset({d}),
    )
    context.sv_fragments = {}


@given('sv a single-deny ScopedBackendView for "{proj}" allow "{r}" deny "{d}"')
def step_sv_view_one_res_denied(context: Any, proj: str, r: str, d: str) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset({proj}),
        resource_ids=frozenset({r}),
        denied_resource_ids=frozenset({d}),
    )
    context.sv_fragments = {}


@then('sv effective_scope should contain "{r1}" and "{r2}" but not "{r3}"')
def step_sv_effective_contains_not(context: Any, r1: str, r2: str, r3: str) -> None:
    eff = context.sv_view.effective_scope()
    assert r1 in eff, f"Expected {r1} in effective_scope"
    assert r2 in eff, f"Expected {r2} in effective_scope"
    assert r3 not in eff, f"Expected {r3} NOT in effective_scope"


@then("sv effective_scope should be empty")
def step_sv_effective_empty(context: Any) -> None:
    eff = context.sv_view.effective_scope()
    assert len(eff) == 0, f"Expected empty effective_scope, got {eff}"


# ---------------------------------------------------------------------------
# from_resource_scope
# ---------------------------------------------------------------------------


@when("sv I create a ScopedBackendView from that ResourceScope")
def step_sv_create_view_from_scope(context: Any) -> None:
    context.sv_view = ScopedBackendView.from_resource_scope(context.sv_scope)


@then('sv the view should allow project "{proj}"')
def step_sv_view_allows_proj(context: Any, proj: str) -> None:
    assert proj in context.sv_view.allowed_projects


@then('sv the view resource_ids should contain "{r1}" and "{r2}"')
def step_sv_view_res_contains(context: Any, r1: str, r2: str) -> None:
    assert r1 in context.sv_view.resource_ids
    assert r2 in context.sv_view.resource_ids


# ---------------------------------------------------------------------------
# ScopedBackendSet
# ---------------------------------------------------------------------------


@given("sv a ScopedBackendSet with text, vector, and graph backends")
def step_sv_backend_set_all(context: Any) -> None:
    context.sv_backend_set = create_scoped_backend_set(
        context.sv_scope,
        text_backend=InMemoryTextBackend(),
        vector_backend=InMemoryVectorBackend(),
        graph_backend=InMemoryGraphBackend(),
    )


@given("sv a ScopedBackendSet with no text backend")
def step_sv_backend_set_no_text(context: Any) -> None:
    context.sv_backend_set = create_scoped_backend_set(
        context.sv_scope,
        text_backend=None,
        vector_backend=InMemoryVectorBackend(),
        graph_backend=InMemoryGraphBackend(),
    )


@when('sv I search text via backend set for "{query}"')
def step_sv_backend_set_search(context: Any, query: str) -> None:
    context.sv_backend_set_results = context.sv_backend_set.search_text(query)


@then("sv the backend set text search should succeed")
def step_sv_backend_set_ok(context: Any) -> None:
    assert isinstance(context.sv_backend_set_results, list)


@then("sv the backend set text search should return empty list")
def step_sv_backend_set_empty(context: Any) -> None:
    assert context.sv_backend_set_results == []


@when("sv I search vector via backend set")
def step_sv_backend_set_vector(context: Any) -> None:
    context.sv_backend_set_vec_results = context.sv_backend_set.search_vector(
        [0.1, 0.2, 0.3]
    )


@then("sv the backend set vector search should return empty list")
def step_sv_backend_set_vec_empty(context: Any) -> None:
    assert context.sv_backend_set_vec_results == []


@when('sv I search graph via backend set for "{query}"')
def step_sv_backend_set_graph(context: Any, query: str) -> None:
    context.sv_backend_set_graph_result = context.sv_backend_set.search_graph(query)


@then("sv the backend set graph search should return empty GraphResult")
def step_sv_backend_set_graph_empty(context: Any) -> None:
    assert context.sv_backend_set_graph_result.triples == []


# ---------------------------------------------------------------------------
# ScopeViolationError
# ---------------------------------------------------------------------------


@when('sv I raise a ScopeViolationError with resource "{res}" and projects "{proj}"')
def step_sv_raise_scope_error(context: Any, res: str, proj: str) -> None:
    context.sv_error = ScopeViolationError(
        "test error",
        resource_ids=(res,),
        scope_project_names=(proj,),
    )


@then('sv the error resource_ids should contain "{res}"')
def step_sv_error_res(context: Any, res: str) -> None:
    assert res in context.sv_error.resource_ids


@then('sv the error scope_project_names should contain "{proj}"')
def step_sv_error_proj(context: Any, proj: str) -> None:
    assert proj in context.sv_error.scope_project_names


# ---------------------------------------------------------------------------
# validate_project_scope
# ---------------------------------------------------------------------------


@then('sv validating projects "{req}" against available "{a1}", "{a2}" should pass')
def step_sv_validate_proj_pass(context: Any, req: str, a1: str, a2: str) -> None:
    validate_project_scope(frozenset({req}), frozenset({a1, a2}))


@then(
    'sv validating projects "{req}" against available "{a1}", "{a2}" should raise ScopeViolationError'
)
def step_sv_validate_proj_fail_one(context: Any, req: str, a1: str, a2: str) -> None:
    try:
        validate_project_scope(frozenset({req}), frozenset({a1, a2}))
        raise AssertionError("Expected ScopeViolationError")
    except ScopeViolationError:
        pass


@then(
    'sv validating mixed projects "{r1}", "{r2}" against available "{a1}", "{a2}" should raise ScopeViolationError'
)
def step_sv_validate_proj_fail_two(
    context: Any, r1: str, r2: str, a1: str, a2: str
) -> None:
    try:
        validate_project_scope(frozenset({r1, r2}), frozenset({a1, a2}))
        raise AssertionError("Expected ScopeViolationError")
    except ScopeViolationError:
        pass


@then("sv validating empty requested projects should raise ValueError")
def step_sv_validate_proj_empty(context: Any) -> None:
    try:
        validate_project_scope(frozenset(), frozenset({"p1"}))
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# validate_resource_scope
# ---------------------------------------------------------------------------


@then('sv validating resources "{res}" against that scope should pass')
def step_sv_validate_res_pass(context: Any, res: str) -> None:
    validate_resource_scope(frozenset({res}), context.sv_scope)


@then(
    'sv validating resources "{res}" against that scope should raise ScopeViolationError'
)
def step_sv_validate_res_fail(context: Any, res: str) -> None:
    try:
        validate_resource_scope(frozenset({res}), context.sv_scope)
        raise AssertionError("Expected ScopeViolationError")
    except ScopeViolationError:
        pass


# ---------------------------------------------------------------------------
# ResourceAliasResolver
# ---------------------------------------------------------------------------


@given('sv a project "{ns}/{name}" with resource "{res}" aliased "{alias}"')
def step_sv_proj_one_alias(
    context: Any, ns: str, name: str, res: str, alias: str
) -> None:
    context.sv_project = _make_project(name, namespace=ns, links=[(res, alias)])


@given(
    'sv a ResourceScope with two resources "{r1}", "{r2}" and two projects "{p1}", "{p2}"'
)
def step_sv_scope_two_res_two_proj(
    context: Any, r1: str, r2: str, p1: str, p2: str
) -> None:
    context.sv_scope = ResourceScope(
        resource_ids=frozenset({r1, r2}),
        project_names=frozenset({p1, p2}),
    )


@given(
    'sv a project "{ns}/{name}" with two resources "{r1}" aliased "{a1}" and "{r2}" aliased "{a2}"'
)
def step_sv_proj_two_aliases(
    context: Any, ns: str, name: str, r1: str, a1: str, r2: str, a2: str
) -> None:
    context.sv_project = _make_project(name, namespace=ns, links=[(r1, a1), (r2, a2)])


@given('sv a project "{ns}/{name}" with duplicate alias "{alias}" on "{r1}" and "{r2}"')
def step_sv_proj_dup_alias(
    context: Any, ns: str, name: str, alias: str, r1: str, r2: str
) -> None:
    context.sv_project = _make_project(
        name, namespace=ns, links=[(r1, alias), (r2, alias)]
    )


@given("sv a ResourceAliasResolver")
def step_sv_resolver(context: Any) -> None:
    context.sv_resolver = ResourceAliasResolver()


@when('sv I resolve alias "{alias}"')
def step_sv_resolve_alias(context: Any, alias: str) -> None:
    context.sv_resolved = context.sv_resolver.resolve(context.sv_project, alias)


@then('sv the resolved resource_id should be "{expected}"')
def step_sv_resolved_is(context: Any, expected: str) -> None:
    assert context.sv_resolved == expected, (
        f"Expected '{expected}', got '{context.sv_resolved}'"
    )


@then("sv the resolved resource_id should be None")
def step_sv_resolved_none(context: Any) -> None:
    assert context.sv_resolved is None


@then("sv resolving empty alias should raise ValueError")
def step_sv_resolve_empty(context: Any) -> None:
    try:
        context.sv_resolver.resolve(context.sv_project, "")
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


@when('sv I resolve many aliases "{a1}", "{a2}"')
def step_sv_resolve_many(context: Any, a1: str, a2: str) -> None:
    context.sv_resolved_set = context.sv_resolver.resolve_many(
        context.sv_project, (a1, a2)
    )


@then('sv the resolved set should contain "{r1}" and "{r2}"')
def step_sv_resolved_set_contains(context: Any, r1: str, r2: str) -> None:
    assert r1 in context.sv_resolved_set
    assert r2 in context.sv_resolved_set


@then('sv the resolved set should contain "{r1}"')
def step_sv_resolved_set_contains_one(context: Any, r1: str) -> None:
    assert r1 in context.sv_resolved_set


@when("sv I validate alias uniqueness")
def step_sv_validate_uniqueness(context: Any) -> None:
    context.sv_uniqueness_errors = context.sv_resolver.validate_uniqueness(
        context.sv_project
    )


@then('sv there should be {count:d} uniqueness error mentioning "{alias}"')
def step_sv_uniqueness_errors_with_alias(context: Any, count: int, alias: str) -> None:
    assert len(context.sv_uniqueness_errors) == count
    if count > 0:
        assert any(alias in err for err in context.sv_uniqueness_errors)


@then("sv there should be {count:d} uniqueness errors")
def step_sv_uniqueness_errors_count(context: Any, count: int) -> None:
    assert len(context.sv_uniqueness_errors) == count


# ---------------------------------------------------------------------------
# resolve_resource_scope
# ---------------------------------------------------------------------------


@given('sv a project "{ns}/{name}" with linked resources "{r1}", "{r2}"')
def step_sv_proj_linked_resources(
    context: Any, ns: str, name: str, r1: str, r2: str
) -> None:
    context.sv_project = _make_project(
        name, namespace=ns, links=[(r1, None), (r2, None)]
    )


@when("sv I resolve the resource scope")
def step_sv_resolve_scope(context: Any) -> None:
    context.sv_scope = resolve_resource_scope([context.sv_project])


@when('sv I resolve the resource scope with include "{inc}"')
def step_sv_resolve_scope_include(context: Any, inc: str) -> None:
    context.sv_scope = resolve_resource_scope(
        [context.sv_project], include_resources=(inc,)
    )


@when('sv I resolve the resource scope with exclude "{exc}"')
def step_sv_resolve_scope_exclude(context: Any, exc: str) -> None:
    context.sv_scope = resolve_resource_scope(
        [context.sv_project], exclude_resources=(exc,)
    )


@then("sv the scope should have {count:d} resource_ids")
def step_sv_scope_res_count(context: Any, count: int) -> None:
    assert len(context.sv_scope.resource_ids) == count, (
        f"Expected {count} resource_ids, got {len(context.sv_scope.resource_ids)}: "
        f"{context.sv_scope.resource_ids}"
    )


@then("sv resolving scope with empty projects should raise ValueError")
def step_sv_resolve_scope_empty(context: Any) -> None:
    try:
        resolve_resource_scope([])
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Coverage: field validator coercion (scoped_view L226, L237)
# ---------------------------------------------------------------------------


@then("sv constructing view with list resource_ids should succeed")
def step_sv_view_list_res(context: Any) -> None:
    view = ScopedBackendView(
        allowed_projects=frozenset({"p"}),
        resource_ids=["R1", "R2"],  # type: ignore[arg-type]
    )
    assert "R1" in view.resource_ids
    assert isinstance(view.resource_ids, frozenset)


@then("sv constructing view with list denied_resource_ids should succeed")
def step_sv_view_list_denied(context: Any) -> None:
    view = ScopedBackendView(
        allowed_projects=frozenset({"p"}),
        resource_ids=frozenset({"R1", "R2"}),
        denied_resource_ids=["R2"],  # type: ignore[arg-type]
    )
    assert "R2" in view.denied_resource_ids
    assert isinstance(view.denied_resource_ids, frozenset)


# ---------------------------------------------------------------------------
# Coverage: _require_effective_scope raises (scoped_view L326-330)
# ---------------------------------------------------------------------------


@then("sv searching text via denied view should raise ScopeViolationError")
def step_sv_search_text_denied(context: Any) -> None:
    try:
        context.sv_view.search_text(context.sv_text_backend, "test")
        raise AssertionError("Expected ScopeViolationError")
    except ScopeViolationError:
        pass


# ---------------------------------------------------------------------------
# Coverage: ScopedBackendSet None backends (scoped_view L489, L505)
# ---------------------------------------------------------------------------


@given("sv a ScopedBackendSet with no vector backend")
def step_sv_backend_set_no_vector(context: Any) -> None:
    context.sv_backend_set_novector = create_scoped_backend_set(
        context.sv_scope,
        text_backend=InMemoryTextBackend(),
        vector_backend=None,
        graph_backend=InMemoryGraphBackend(),
    )


@then("sv searching vector via no-vector set should return empty list")
def step_sv_novector_search(context: Any) -> None:
    result = context.sv_backend_set_novector.search_vector([0.1])
    assert result == []


@given("sv a ScopedBackendSet with no graph backend")
def step_sv_backend_set_no_graph(context: Any) -> None:
    context.sv_backend_set_nograph = create_scoped_backend_set(
        context.sv_scope,
        text_backend=InMemoryTextBackend(),
        vector_backend=InMemoryVectorBackend(),
        graph_backend=None,
    )


@then("sv searching graph via no-graph set should return empty GraphResult")
def step_sv_nograph_search(context: Any) -> None:
    result = context.sv_backend_set_nograph.search_graph("SELECT ?s")
    assert result.triples == []


# ---------------------------------------------------------------------------
# Coverage: resolve_many unresolvable (scoped_view L617)
# ---------------------------------------------------------------------------


@then("sv the resolved set size should be {count:d}")
def step_sv_resolved_set_size(context: Any, count: int) -> None:
    assert len(context.sv_resolved_set) == count, (
        f"Expected {count} entries, got {len(context.sv_resolved_set)}"
    )


# ---------------------------------------------------------------------------
# Coverage: validate_uniqueness with no-alias resource (scoped_view L638)
# ---------------------------------------------------------------------------


@given("sv a project with aliased and unaliased resources")
def step_sv_proj_mixed_alias(context: Any) -> None:
    context.sv_project = _make_project(
        "myproj",
        namespace="local",
        links=[
            ("01HQ8ZDRX50000000000000001", "repo"),
            ("01HQ8ZDRX50000000000000002", None),
        ],
    )


# ---------------------------------------------------------------------------
# Coverage: validate_project_scope empty available
# ---------------------------------------------------------------------------


@then("sv validating projects against empty available should raise ValueError")
def step_sv_validate_proj_empty_avail(context: Any) -> None:
    try:
        validate_project_scope(frozenset({"p1"}), frozenset())
        raise AssertionError("Expected ValueError")
    except ValueError:
        pass


# ---------------------------------------------------------------------------
# Coverage: validate_resource_scope empty requested
# ---------------------------------------------------------------------------


@then("sv validating empty resources against scope should pass")
def step_sv_validate_res_empty_req(context: Any) -> None:
    validate_resource_scope(frozenset(), context.sv_scope)


# ---------------------------------------------------------------------------
# P0-S1: is_visible empty resource_id deny-by-default
# ---------------------------------------------------------------------------


@given('sv a ScopedBackendView for projects "{proj}" with resource_ids "{rid}"')
def step_sv_view_with_resources(context: Any, proj: str, rid: str) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset([proj]),
        resource_ids=frozenset([rid]),
    )


@given('sv a ScopedBackendView for project "{proj}"')
def step_sv_view_project_only(context: Any, proj: str) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=frozenset([proj]),
    )


@when('sv I check visibility of a fragment with project "{proj}" and empty resource_id')
def step_sv_check_empty_rid(context: Any, proj: str) -> None:
    frag = TieredFragment(
        fragment_id="empty-rid-frag",
        content="test",
        project_name=proj,
        resource_id="",
        tier=ContextTier.HOT,
    )
    context.sv_vis_result = context.sv_view.is_visible(frag)


@then("sv the fragment should NOT be visible")
def step_sv_not_visible(context: Any) -> None:
    assert context.sv_vis_result is False


@then("sv the fragment should be visible")
def step_sv_is_visible(context: Any) -> None:
    assert context.sv_vis_result is True


# ---------------------------------------------------------------------------
# P2-B2: tuple coercion
# ---------------------------------------------------------------------------


@when("sv I create a ScopedBackendView with tuple allowed_projects")
def step_sv_tuple_projects(context: Any) -> None:
    context.sv_view = ScopedBackendView(
        allowed_projects=("proj-a",),
        resource_ids=frozenset(),
    )


@then("sv the view should have {count:d} allowed project")
def step_sv_view_project_count(context: Any, count: int) -> None:
    assert len(context.sv_view.allowed_projects) == count
