"""Step definitions for vector_store_service_cov3.feature - round-3 coverage."""

from __future__ import annotations

import shutil
import sys
import tempfile
import types
from pathlib import Path
from typing import Any, ClassVar
from unittest.mock import MagicMock, patch

from behave import given, then, when
from behave.runner import Context
from langchain_community.embeddings import FakeEmbeddings

from cleveragents.application.services.config_service import (
    ConfigLevel,
    ConfigService,
    ResolvedValue,
)
from cleveragents.application.services.vector_store_service import (
    VectorStoreService,
    _FaissAcmsStoreProtocol,
    _FaissDocstoreProtocol,
    _FaissPlanStoreProtocol,
)
from cleveragents.config.settings import Settings
from cleveragents.core.exceptions import ConfigurationError

# ---------------------------------------------------------------------------
# Concrete subclasses of Protocol stubs to exercise their method bodies
# ---------------------------------------------------------------------------


class _ConcretePlanStore(_FaissPlanStoreProtocol):  # type: ignore[misc]
    """Concrete subclass that delegates to Protocol method bodies."""

    def similarity_search_with_score(self, query, *, k):  # type: ignore[override]
        return _FaissPlanStoreProtocol.similarity_search_with_score(self, query, k=k)

    def save_local(self, folder_path):  # type: ignore[override]
        return _FaissPlanStoreProtocol.save_local(self, folder_path)


class _ConcreteDocstore(_FaissDocstoreProtocol):  # type: ignore[misc]
    """Concrete subclass that delegates to Protocol method body."""

    def search(self, key):  # type: ignore[override]
        return _FaissDocstoreProtocol.search(self, key)


class _ConcreteAcmsStore(_FaissAcmsStoreProtocol):  # type: ignore[misc]
    """Concrete subclass that delegates to Protocol method bodies."""

    index_to_docstore_id: ClassVar[dict] = {}  # type: ignore[assignment]
    docstore = None  # type: ignore[assignment]

    def add_embeddings(self, text_embeddings, *, metadatas, ids):  # type: ignore[override]
        return _FaissAcmsStoreProtocol.add_embeddings(
            self, text_embeddings, metadatas=metadatas, ids=ids
        )

    def similarity_search_with_score_by_vector(  # type: ignore[override]
        self, embedding, *, k, filter=None, fetch_k
    ):
        return _FaissAcmsStoreProtocol.similarity_search_with_score_by_vector(
            self, embedding, k=k, filter=filter, fetch_k=fetch_k
        )

    def delete(self, *, ids):  # type: ignore[override]
        return _FaissAcmsStoreProtocol.delete(self, ids=ids)

    def save_local(self, folder_path):  # type: ignore[override]
        return _FaissAcmsStoreProtocol.save_local(self, folder_path)

    def similarity_search_with_score(self, query, *, k):  # type: ignore[override]
        return _FaissPlanStoreProtocol.similarity_search_with_score(self, query, k=k)


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


def _add_cleanup(context: Context, handler) -> None:
    """Register a cleanup handler on the behave context."""
    if not hasattr(context, "_cleanup_handlers"):
        context._cleanup_handlers = []
    context._cleanup_handlers.append(handler)


class _StubDocument:
    """Minimal document with metadata and page_content."""

    def __init__(self, content: str, metadata: dict[str, Any] | None = None) -> None:
        self.page_content = content
        self.metadata = metadata or {}


class _AcmsRecordingStore:
    """Test-double for a FAISS ACMS store with enough API surface."""

    def __init__(self) -> None:
        self.index_to_docstore_id: dict[int, str] = {}
        self.docstore = _RecordingDocstore()
        self._deleted_ids: list[str] = []
        self._added: list[
            tuple[list[tuple[str, list[float]]], list[dict[str, str]], list[str]]
        ] = []
        self._saved_dirs: list[str] = []
        self._search_results: list[tuple[_StubDocument, float]] = []

    def add_embeddings(
        self,
        text_embeddings: list[tuple[str, list[float]]],
        *,
        metadatas: list[dict[str, str]],
        ids: list[str],
    ) -> None:
        self._added.append((text_embeddings, metadatas, ids))
        # Add to index_to_docstore_id to keep the store populated
        next_idx = max(self.index_to_docstore_id.keys(), default=-1) + 1
        for store_id in ids:
            self.index_to_docstore_id[next_idx] = store_id
            next_idx += 1

    def delete(self, *, ids: list[str]) -> None:
        self._deleted_ids.extend(ids)
        # Remove from index_to_docstore_id
        self.index_to_docstore_id = {
            k: v for k, v in self.index_to_docstore_id.items() if v not in ids
        }

    def save_local(self, folder_path: str) -> None:
        self._saved_dirs.append(folder_path)

    def similarity_search_with_score(
        self, query: str, *, k: int
    ) -> list[tuple[Any, Any]]:
        return self._search_results[:k]

    def similarity_search_with_score_by_vector(
        self,
        embedding: list[float],
        *,
        k: int,
        filter: Any = None,
        fetch_k: int = 20,
    ) -> list[tuple[Any, Any]]:
        results = list(self._search_results)
        if filter is not None:
            results = [(doc, score) for doc, score in results if filter(doc.metadata)]
        return results[:k]


class _RecordingDocstore:
    """Minimal docstore test-double."""

    def __init__(self) -> None:
        self._docs: dict[str, _StubDocument] = {}

    def add(self, doc_id: str, doc: _StubDocument) -> None:
        self._docs[doc_id] = doc

    def search(self, key: str) -> Any:
        return self._docs.get(key, _StubDocument("", {}))


class _StubOpenAIEmbeddings:
    """Captures construction kwargs for verification."""

    last_model: str | None = None
    last_kwargs: ClassVar[dict[str, Any]] = {}

    def __init__(self, **kwargs: Any) -> None:
        _StubOpenAIEmbeddings.last_model = kwargs.get("model")
        _StubOpenAIEmbeddings.last_kwargs = dict(kwargs)


class _RecordingFaissForAcms:
    """Minimal FAISS stand-in for ACMS operations."""

    load_local_should_raise: bool = False
    last_from_embeddings_args: dict[str, Any] | None = None
    last_load_local_instance: _AcmsRecordingStore | None = None

    @classmethod
    def reset(cls) -> None:
        cls.load_local_should_raise = False
        cls.last_from_embeddings_args = None
        cls.last_load_local_instance = None

    @classmethod
    def from_embeddings(
        cls,
        text_embeddings: list[tuple[str, list[float]]],
        embedding: Any,
        *,
        metadatas: list[dict[str, str]] | None = None,
        ids: list[str] | None = None,
    ) -> _AcmsRecordingStore:
        cls.last_from_embeddings_args = {
            "text_embeddings": text_embeddings,
            "metadatas": metadatas,
            "ids": ids,
        }
        store = _AcmsRecordingStore()
        if ids:
            for idx, sid in enumerate(ids):
                store.index_to_docstore_id[idx] = sid
        return store

    @classmethod
    def load_local(
        cls,
        directory: str,
        embeddings: Any,
        allow_dangerous_deserialization: bool = True,
    ) -> _AcmsRecordingStore:
        if cls.load_local_should_raise:
            raise ValueError("simulated load failure")
        store = _AcmsRecordingStore()
        cls.last_load_local_instance = store
        return store


def _make_config_service_stub(resolve_map: dict[str, Any]) -> ConfigService:
    """Create a ConfigService whose resolve() returns canned values."""
    svc = MagicMock(spec=ConfigService)

    def _resolve(key: str, **_kw: Any) -> ResolvedValue:
        return ResolvedValue(
            key=key,
            value=resolve_map.get(key),
            source=ConfigLevel.DEFAULT,
        )

    svc.resolve = _resolve
    return svc


def _make_service(
    context: Context,
    *,
    acms_backend: str = "faiss",
    embedding_provider: str = "fake",
    embedding_model: str | None = None,
    embedding_dims: int | None = None,
) -> VectorStoreService:
    """Build a VectorStoreService wired to test doubles."""
    temp_dir = tempfile.mkdtemp(prefix="vscov3-")
    _add_cleanup(context, lambda: shutil.rmtree(temp_dir, ignore_errors=True))

    settings = Settings()
    settings.vector_store_enabled = True
    settings.vector_store_path = Path(temp_dir) / "vector_store"
    settings.vector_embeddings_provider = "fake"
    settings.vector_embeddings_dimension = 8

    acms_dir = Path(temp_dir) / "acms_store"
    acms_dir.mkdir(parents=True, exist_ok=True)

    resolve_map: dict[str, Any] = {
        "index.vector.backend": acms_backend,
        "index.vector.dir": str(acms_dir),
        "index.embedding.provider": embedding_provider,
        "index.embedding.model": embedding_model,
        "index.embedding.dimensions": embedding_dims,
    }
    config_svc = _make_config_service_stub(resolve_map)

    svc = VectorStoreService(settings, MagicMock(), config_service=config_svc)
    context.vscov3_service = svc
    context.vscov3_error = None
    context.vscov3_result = None
    context.vscov3_temp_dir = temp_dir
    context.vscov3_acms_dir = acms_dir
    context.vscov3_resolve_map = resolve_map
    context.vscov3_config_svc = config_svc
    return svc


# ===================================================================
# Protocol body coverage
# ===================================================================


@given("vscov3 a plan store protocol instance")
def step_vscov3_plan_protocol(context: Context) -> None:
    context.vscov3_protocol = _ConcretePlanStore()
    context.vscov3_error = None


@given("vscov3 a docstore protocol instance")
def step_vscov3_docstore_protocol(context: Context) -> None:
    context.vscov3_protocol = _ConcreteDocstore()
    context.vscov3_error = None


@given("vscov3 an ACMS store protocol instance")
def step_vscov3_acms_protocol(context: Context) -> None:
    context.vscov3_protocol = _ConcreteAcmsStore()
    context.vscov3_error = None


@when("vscov3 I call similarity_search_with_score on the plan protocol")
def step_vscov3_call_plan_similarity(context: Context) -> None:
    try:
        context.vscov3_protocol.similarity_search_with_score("q", k=1)
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call save_local on the plan protocol")
def step_vscov3_call_plan_save(context: Context) -> None:
    try:
        context.vscov3_protocol.save_local("/tmp/x")
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call search on the docstore protocol")
def step_vscov3_call_docstore_search(context: Context) -> None:
    try:
        context.vscov3_protocol.search("key")
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call add_embeddings on the ACMS protocol")
def step_vscov3_call_acms_add(context: Context) -> None:
    try:
        context.vscov3_protocol.add_embeddings(
            [("text", [0.1])], metadatas=[{"k": "v"}], ids=["id1"]
        )
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call similarity_search_with_score_by_vector on the ACMS protocol")
def step_vscov3_call_acms_search_vec(context: Context) -> None:
    try:
        context.vscov3_protocol.similarity_search_with_score_by_vector(
            [0.1], k=1, fetch_k=5
        )
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call delete on the ACMS protocol")
def step_vscov3_call_acms_delete(context: Context) -> None:
    try:
        context.vscov3_protocol.delete(ids=["id1"])
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@when("vscov3 I call save_local on the ACMS protocol")
def step_vscov3_call_acms_save(context: Context) -> None:
    try:
        context.vscov3_protocol.save_local("/tmp/y")
    except NotImplementedError:
        context.vscov3_error = NotImplementedError("raised")


@then("vscov3 the plan protocol should raise NotImplementedError")
def step_vscov3_verify_plan_nie(context: Context) -> None:
    assert isinstance(context.vscov3_error, NotImplementedError)


@then("vscov3 the ACMS protocol should raise NotImplementedError")
def step_vscov3_verify_acms_nie(context: Context) -> None:
    assert isinstance(context.vscov3_error, NotImplementedError)


@then("vscov3 the docstore protocol should raise NotImplementedError")
def step_vscov3_verify_docstore_nie(context: Context) -> None:
    assert isinstance(context.vscov3_error, NotImplementedError)


# ===================================================================
# _require_faiss when FAISS is None
# ===================================================================


@given("vscov3 a vector store service with ACMS enabled")
def step_vscov3_service_acms_enabled(context: Context) -> None:
    _make_service(context, acms_backend="faiss")


@given("vscov3 a vector store service with ACMS disabled")
def step_vscov3_service_acms_disabled(context: Context) -> None:
    _make_service(context, acms_backend="none")


@given("vscov3 FAISS is patched to None")
def step_vscov3_faiss_none(context: Context) -> None:
    patcher = patch(
        "cleveragents.application.services.vector_store_service.FAISS",
        None,
    )
    patcher.start()
    _add_cleanup(context, patcher.stop)


@when("vscov3 I call _require_faiss")
def step_vscov3_call_require_faiss(context: Context) -> None:
    try:
        context.vscov3_service._require_faiss()
    except ConfigurationError as exc:
        context.vscov3_error = exc


@then("vscov3 a ConfigurationError mentioning FAISS should be stored")
def step_vscov3_verify_faiss_error(context: Context) -> None:
    assert isinstance(context.vscov3_error, ConfigurationError)
    assert "faiss" in str(context.vscov3_error).lower()


# ===================================================================
# ACMS count
# ===================================================================


@given('vscov3 the ACMS store has indexed documents for projects "alpha" and "beta"')
def step_vscov3_acms_store_with_docs(context: Context) -> None:
    store = _AcmsRecordingStore()
    store.index_to_docstore_id = {0: "alpha::d1", 1: "beta::d2"}
    doc_alpha = _StubDocument("alpha content", {"project": "alpha", "doc_id": "d1"})
    doc_beta = _StubDocument("beta content", {"project": "beta", "doc_id": "d2"})
    store.docstore.add("alpha::d1", doc_alpha)
    store.docstore.add("beta::d2", doc_beta)
    context.vscov3_service._acms_store = store


@when("vscov3 I call acms_count without a project filter")
def step_vscov3_acms_count_no_filter(context: Context) -> None:
    try:
        context.vscov3_result = context.vscov3_service.acms_count()
    except Exception as exc:
        context.vscov3_error = exc


@when('vscov3 I call acms_count for project "{project}"')
def step_vscov3_acms_count_filtered(context: Context, project: str) -> None:
    try:
        context.vscov3_result = context.vscov3_service.acms_count(project=project)
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the ACMS count should be {expected:d}")
def step_vscov3_verify_count(context: Context, expected: int) -> None:
    assert context.vscov3_result == expected, (
        f"Expected {expected}, got {context.vscov3_result}"
    )


# ===================================================================
# acms_index_embedding validations
# ===================================================================


@when("vscov3 I index an ACMS embedding with an empty project")
def step_vscov3_index_empty_project(context: Context) -> None:
    try:
        context.vscov3_service.acms_index_embedding(
            project="",
            doc_id="d1",
            embedding=[0.1],
            metadata={},
        )
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@when("vscov3 I index an ACMS embedding with an empty doc_id")
def step_vscov3_index_empty_doc_id(context: Context) -> None:
    try:
        context.vscov3_service.acms_index_embedding(
            project="p1",
            doc_id="",
            embedding=[0.1],
            metadata={},
        )
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@when("vscov3 I index an ACMS embedding with an empty embedding list")
def step_vscov3_index_empty_embedding(context: Context) -> None:
    try:
        context.vscov3_service.acms_index_embedding(
            project="p1",
            doc_id="d1",
            embedding=[],
            metadata={},
        )
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@when("vscov3 I index an ACMS embedding with valid inputs")
def step_vscov3_index_valid_disabled(context: Context) -> None:
    try:
        context.vscov3_service.acms_index_embedding(
            project="p1",
            doc_id="d1",
            embedding=[0.1],
            metadata={},
        )
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@then('vscov3 a ValueError mentioning "{keyword}" should be stored')
def step_vscov3_verify_value_error(context: Context, keyword: str) -> None:
    assert isinstance(context.vscov3_error, ValueError), (
        f"Expected ValueError, got {type(context.vscov3_error).__name__}: {context.vscov3_error}"
    )
    assert keyword.lower() in str(context.vscov3_error).lower(), (
        f"Expected '{keyword}' in error message: {context.vscov3_error}"
    )


@then('vscov3 a ConfigurationError mentioning "{keyword}" should be stored')
def step_vscov3_verify_config_error(context: Context, keyword: str) -> None:
    assert isinstance(context.vscov3_error, ConfigurationError), (
        f"Expected ConfigurationError, got {type(context.vscov3_error).__name__}: {context.vscov3_error}"
    )
    assert keyword.lower() in str(context.vscov3_error).lower(), (
        f"Expected '{keyword}' in error message: {context.vscov3_error}"
    )


# ===================================================================
# acms_index_embedding - content fallback and add-to-existing
# ===================================================================


@given("vscov3 FAISS is mocked for ACMS operations")
def step_vscov3_mock_faiss_acms(context: Context) -> None:
    _RecordingFaissForAcms.reset()
    patcher = patch(
        "cleveragents.application.services.vector_store_service.FAISS",
        _RecordingFaissForAcms,
    )
    patcher.start()
    _add_cleanup(context, patcher.stop)
    context.vscov3_faiss = _RecordingFaissForAcms


@when(
    'vscov3 I index an embedding with project "{project}" doc_id "{doc_id}" embedding [{emb}] and no content'
)
def step_vscov3_index_no_content(
    context: Context, project: str, doc_id: str, emb: str
) -> None:
    embedding = [float(x.strip()) for x in emb.split(",")]
    try:
        context.vscov3_service.acms_index_embedding(
            project=project,
            doc_id=doc_id,
            embedding=embedding,
            metadata={},
            content=None,
        )
    except Exception as exc:
        context.vscov3_error = exc


@then('vscov3 the indexed payload should be "{expected}"')
def step_vscov3_verify_payload(context: Context, expected: str) -> None:
    args = _RecordingFaissForAcms.last_from_embeddings_args
    assert args is not None, "No from_embeddings call recorded"
    text_embeddings = args["text_embeddings"]
    assert len(text_embeddings) == 1
    payload_text, _vec = text_embeddings[0]
    assert payload_text == expected, f"Expected '{expected}', got '{payload_text}'"


@given('vscov3 the ACMS store already has a document "{store_id}"')
def step_vscov3_existing_acms_doc(context: Context, store_id: str) -> None:
    store = _AcmsRecordingStore()
    store.index_to_docstore_id = {0: store_id}
    doc = _StubDocument("old content", {"project": "p1", "doc_id": "d1"})
    store.docstore.add(store_id, doc)
    context.vscov3_service._acms_store = store
    context.vscov3_existing_store = store


@when(
    'vscov3 I index an embedding with project "{project}" doc_id "{doc_id}" embedding [{emb}] and content "{content}"'
)
def step_vscov3_index_with_content(
    context: Context, project: str, doc_id: str, emb: str, content: str
) -> None:
    embedding = [float(x.strip()) for x in emb.split(",")]
    try:
        context.vscov3_service.acms_index_embedding(
            project=project,
            doc_id=doc_id,
            embedding=embedding,
            metadata={},
            content=content,
        )
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the existing store_id should have been deleted before re-add")
def step_vscov3_verify_delete_before_add(context: Context) -> None:
    store = context.vscov3_existing_store
    assert "p1::d1" in store._deleted_ids, (
        f"Expected 'p1::d1' in deleted ids: {store._deleted_ids}"
    )


@then("vscov3 the store should have received add_embeddings")
def step_vscov3_verify_add_embeddings(context: Context) -> None:
    store = context.vscov3_existing_store
    assert len(store._added) > 0, "No add_embeddings calls recorded"


# ===================================================================
# acms_search_by_vector
# ===================================================================


@when("vscov3 I search ACMS with empty embedding")
def step_vscov3_search_empty_embedding(context: Context) -> None:
    try:
        context.vscov3_result = context.vscov3_service.acms_search_by_vector([])
    except ValueError as exc:
        context.vscov3_error = exc


@when("vscov3 I search ACMS with top_k {top_k:d}")
def step_vscov3_search_bad_topk(context: Context, top_k: int) -> None:
    try:
        context.vscov3_result = context.vscov3_service.acms_search_by_vector(
            [0.1, 0.2], top_k=top_k
        )
    except ValueError as exc:
        context.vscov3_error = exc


@when("vscov3 I search ACMS with embedding [{emb}] and top_k {top_k:d}")
def step_vscov3_search_acms(context: Context, emb: str, top_k: int) -> None:
    embedding = [float(x.strip()) for x in emb.split(",")]
    try:
        context.vscov3_result = context.vscov3_service.acms_search_by_vector(
            embedding, top_k=top_k
        )
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the ACMS search results should be empty")
def step_vscov3_search_empty(context: Context) -> None:
    assert context.vscov3_result == [], f"Expected [], got {context.vscov3_result}"


@given('vscov3 the ACMS store has search results with resource_ids "{r1}" and "{r2}"')
def step_vscov3_acms_store_search_results(context: Context, r1: str, r2: str) -> None:
    store = _AcmsRecordingStore()
    store.index_to_docstore_id = {0: "s1", 1: "s2"}
    store._search_results = [
        (_StubDocument("content1", {"resource_id": r1, "project": "p1"}), 0.5),
        (_StubDocument("content2", {"resource_id": r2, "project": "p1"}), 0.8),
    ]
    context.vscov3_service._acms_store = store


@when('vscov3 I search ACMS with embedding [{emb}] scope "{scope}" and top_k {top_k:d}')
def step_vscov3_search_with_scope(
    context: Context, emb: str, scope: str, top_k: int
) -> None:
    embedding = [float(x.strip()) for x in emb.split(",")]
    scope_set = frozenset(scope.split(","))
    try:
        context.vscov3_result = context.vscov3_service.acms_search_by_vector(
            embedding, top_k=top_k, scope=scope_set
        )
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the ACMS search results should have entries")
def step_vscov3_search_has_entries(context: Context) -> None:
    assert isinstance(context.vscov3_result, list)
    assert len(context.vscov3_result) > 0, "Expected non-empty results"


# ===================================================================
# acms_remove_embedding
# ===================================================================


@when("vscov3 I remove an ACMS embedding with an empty project")
def step_vscov3_remove_empty_project(context: Context) -> None:
    try:
        context.vscov3_service.acms_remove_embedding("", "d1")
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@when("vscov3 I remove an ACMS embedding with an empty doc_id")
def step_vscov3_remove_empty_doc_id(context: Context) -> None:
    try:
        context.vscov3_service.acms_remove_embedding("p1", "")
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@when('vscov3 I remove ACMS embedding for project "{project}" and doc_id "{doc_id}"')
def step_vscov3_remove_embedding(context: Context, project: str, doc_id: str) -> None:
    try:
        context.vscov3_service.acms_remove_embedding(project, doc_id)
    except (ValueError, ConfigurationError) as exc:
        context.vscov3_error = exc


@then("vscov3 no error should be stored")
def step_vscov3_no_error(context: Context) -> None:
    assert context.vscov3_error is None, f"Unexpected error: {context.vscov3_error}"


@given('vscov3 the ACMS store has documents "{id1}" and "{id2}"')
def step_vscov3_acms_store_two_docs(context: Context, id1: str, id2: str) -> None:
    store = _AcmsRecordingStore()
    store.index_to_docstore_id = {0: id1, 1: id2}
    doc1 = _StubDocument("c1", {"project": "p1", "doc_id": "d1"})
    doc2 = _StubDocument("c2", {"project": "p1", "doc_id": "d2"})
    store.docstore.add(id1, doc1)
    store.docstore.add(id2, doc2)
    context.vscov3_service._acms_store = store
    context.vscov3_existing_store = store


@then("vscov3 the ACMS store should still be set")
def step_vscov3_store_still_set(context: Context) -> None:
    assert context.vscov3_service._acms_store is not None, (
        "Expected _acms_store to remain set"
    )


@then("vscov3 save should have been called")
def step_vscov3_verify_save_called(context: Context) -> None:
    store = context.vscov3_service._acms_store
    if hasattr(store, "_saved_dirs"):
        assert len(store._saved_dirs) > 0, "Expected save_local to have been called"


# ===================================================================
# _create_acms_embeddings
# ===================================================================


@given(
    'vscov3 the ACMS embedding config uses provider "{provider}" with no model and no dimensions'
)
def step_vscov3_acms_config_no_dims(context: Context, provider: str) -> None:
    # Update the resolve map to change the provider and remove dimensions
    context.vscov3_resolve_map["index.embedding.provider"] = provider
    context.vscov3_resolve_map["index.embedding.dimensions"] = None
    context.vscov3_resolve_map["index.embedding.model"] = None


@given(
    'vscov3 the ACMS embedding config uses provider "{provider}" model "{model}" and dimensions {dims:d}'
)
def step_vscov3_acms_config_with_dims(
    context: Context, provider: str, model: str, dims: int
) -> None:
    context.vscov3_resolve_map["index.embedding.provider"] = provider
    context.vscov3_resolve_map["index.embedding.model"] = model
    context.vscov3_resolve_map["index.embedding.dimensions"] = dims


@given(
    'vscov3 the ACMS embedding config uses provider "{provider}" model "{model}" with no dimensions'
)
def step_vscov3_acms_config_model_no_dims(
    context: Context, provider: str, model: str
) -> None:
    context.vscov3_resolve_map["index.embedding.provider"] = provider
    context.vscov3_resolve_map["index.embedding.model"] = model
    context.vscov3_resolve_map["index.embedding.dimensions"] = None


@given("vscov3 a stub OpenAI embeddings module is available")
def step_vscov3_stub_openai(context: Context) -> None:
    _StubOpenAIEmbeddings.last_model = None
    _StubOpenAIEmbeddings.last_kwargs = {}
    module = types.SimpleNamespace(OpenAIEmbeddings=_StubOpenAIEmbeddings)
    previous = sys.modules.get("langchain_openai")
    sys.modules["langchain_openai"] = module

    def cleanup() -> None:
        if previous is None:
            sys.modules.pop("langchain_openai", None)
        else:
            sys.modules["langchain_openai"] = previous

    _add_cleanup(context, cleanup)


@when("vscov3 I call _create_acms_embeddings")
def step_vscov3_call_create_acms_embeddings(context: Context) -> None:
    try:
        context.vscov3_result = context.vscov3_service._create_acms_embeddings()
    except (ConfigurationError, Exception) as exc:
        context.vscov3_error = exc


@then("vscov3 the embeddings should be FakeEmbeddings with default dimension")
def step_vscov3_verify_fake_embeddings(context: Context) -> None:
    assert isinstance(context.vscov3_result, FakeEmbeddings), (
        f"Expected FakeEmbeddings, got {type(context.vscov3_result)}"
    )
    assert (
        context.vscov3_result.size
        == context.vscov3_service.settings.vector_embeddings_dimension
    )


@then('vscov3 the embeddings should be StubOpenAIEmbeddings with model "{model}"')
def step_vscov3_verify_openai_embeddings(context: Context, model: str) -> None:
    assert isinstance(context.vscov3_result, _StubOpenAIEmbeddings), (
        f"Expected _StubOpenAIEmbeddings, got {type(context.vscov3_result)}"
    )
    assert _StubOpenAIEmbeddings.last_model == model, (
        f"Expected model '{model}', got '{_StubOpenAIEmbeddings.last_model}'"
    )


# ===================================================================
# _load_acms_store
# ===================================================================


@when("vscov3 I call _load_acms_store")
def step_vscov3_call_load_acms(context: Context) -> None:
    try:
        context.vscov3_result = context.vscov3_service._load_acms_store()
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the loaded store should be None")
def step_vscov3_loaded_none(context: Context) -> None:
    assert context.vscov3_result is None, f"Expected None, got {context.vscov3_result}"


@then("vscov3 the loaded store should not be None")
def step_vscov3_loaded_not_none(context: Context) -> None:
    assert context.vscov3_result is not None, "Expected a store, got None"


@given("vscov3 the ACMS store directory has index files")
def step_vscov3_create_index_files(context: Context) -> None:
    acms_dir = context.vscov3_acms_dir
    (acms_dir / "index.faiss").write_bytes(b"stub")
    (acms_dir / "index.pkl").write_bytes(b"stub")


@given("vscov3 FAISS load_local will raise ValueError")
def step_vscov3_faiss_load_raises(context: Context) -> None:
    _RecordingFaissForAcms.load_local_should_raise = True

    def cleanup() -> None:
        _RecordingFaissForAcms.load_local_should_raise = False

    _add_cleanup(context, cleanup)


# ===================================================================
# _save_acms_store early return
# ===================================================================


# ===================================================================
# _acms_store_dir with relative path (line 456)
# ===================================================================


@given("vscov3 a vector store service with ACMS dir set to a relative path")
def step_vscov3_service_relative_acms_dir(context: Context) -> None:
    temp_dir = tempfile.mkdtemp(prefix="vscov3-relpath-")
    _add_cleanup(context, lambda: shutil.rmtree(temp_dir, ignore_errors=True))

    settings = Settings()
    settings.vector_store_enabled = True
    settings.vector_store_path = Path(temp_dir) / "vector_store"
    settings.vector_embeddings_provider = "fake"
    settings.vector_embeddings_dimension = 8

    resolve_map: dict[str, Any] = {
        "index.vector.backend": "faiss",
        "index.vector.dir": "relative_acms_dir",  # relative path!
        "index.embedding.provider": "fake",
        "index.embedding.model": None,
        "index.embedding.dimensions": None,
    }
    config_svc = _make_config_service_stub(resolve_map)
    svc = VectorStoreService(settings, MagicMock(), config_service=config_svc)
    context.vscov3_service = svc
    context.vscov3_error = None
    context.vscov3_result = None


@when("vscov3 I call _acms_store_dir")
def step_vscov3_call_acms_store_dir(context: Context) -> None:
    try:
        context.vscov3_result = context.vscov3_service._acms_store_dir()
    except Exception as exc:
        context.vscov3_error = exc


@then("vscov3 the returned path should be absolute")
def step_vscov3_verify_absolute_path(context: Context) -> None:
    assert isinstance(context.vscov3_result, Path), (
        f"Expected Path, got {type(context.vscov3_result)}"
    )
    assert context.vscov3_result.is_absolute(), (
        f"Expected absolute path, got {context.vscov3_result}"
    )


@when("vscov3 I call _save_acms_store")
def step_vscov3_call_save_acms(context: Context) -> None:
    try:
        context.vscov3_service._save_acms_store()
    except Exception as exc:
        context.vscov3_error = exc


# ===================================================================
# _iter_store_metadata
# ===================================================================


@given("vscov3 a mock ACMS store with docstore entries")
def step_vscov3_mock_store_docstore(context: Context) -> None:
    store = _AcmsRecordingStore()
    store.index_to_docstore_id = {0: "p1::d1", 1: "p2::d2"}
    doc1 = _StubDocument("c1", {"project": "p1", "doc_id": "d1", "location": "loc1"})
    doc2 = _StubDocument("c2", {"project": "p2", "doc_id": "d2", "location": "loc2"})
    store.docstore.add("p1::d1", doc1)
    store.docstore.add("p2::d2", doc2)
    context.vscov3_mock_store = store


@when("vscov3 I call _iter_store_metadata")
def step_vscov3_call_iter_metadata(context: Context) -> None:
    try:
        results = list(
            context.vscov3_service._iter_store_metadata(context.vscov3_mock_store)
        )
        context.vscov3_result = results
    except Exception as exc:
        context.vscov3_error = exc


@then(
    "vscov3 the iteration should yield {expected:d} entries with string keys and values"
)
def step_vscov3_verify_iter_metadata(context: Context, expected: int) -> None:
    assert isinstance(context.vscov3_result, list), (
        f"Expected list, got {type(context.vscov3_result)}"
    )
    assert len(context.vscov3_result) == expected, (
        f"Expected {expected} entries, got {len(context.vscov3_result)}"
    )
    for store_id, metadata in context.vscov3_result:
        assert isinstance(store_id, str)
        assert isinstance(metadata, dict)
        for k, v in metadata.items():
            assert isinstance(k, str), f"Key {k!r} is not str"
            assert isinstance(v, str), f"Value {v!r} is not str"
