"""Test doubles for UKO Indexer BDD and Robot tests.

Provides in-memory content readers, failing backends, tracking
lifecycle hooks, and event bus stubs used by
``features/steps/uko_indexer_steps.py``,
``robot/helper_uko_indexer.py``, and ``benchmarks/uko_indexer_bench.py``.
"""

from __future__ import annotations

import threading
from collections.abc import Callable
from typing import Any

from cleveragents.domain.models.acms.index_backends import (
    IndexedDocument,
    SearchResult,
)
from cleveragents.domain.models.acms.provenance import IndexResult
from cleveragents.domain.models.core.resource import Resource
from cleveragents.infrastructure.events.models import DomainEvent
from cleveragents.infrastructure.events.types import EventType

# ---------------------------------------------------------------------------
# Content readers
# ---------------------------------------------------------------------------


class InMemoryContentReader:
    """Content reader that returns pre-configured content strings."""

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

    def set_content(self, resource_id: str, content: str) -> None:
        self._content[resource_id] = content

    def read_content(self, resource: Resource) -> str:
        if resource.resource_id in self._content:
            return self._content[resource.resource_id]
        raise OSError(f"No content for {resource.resource_id}")


class FailingContentReader:
    """Content reader that always raises OSError."""

    def read_content(self, resource: Resource) -> str:
        raise OSError("Simulated read failure")


# ---------------------------------------------------------------------------
# Lifecycle hooks
# ---------------------------------------------------------------------------


class TrackingLifecycleHook:
    """Lifecycle hook that records events for assertion."""

    def __init__(self) -> None:
        self.indexed_events: list[IndexResult] = []
        self.removed_events: list[tuple[str, str]] = []
        self.error_events: list[tuple[str, str]] = []

    def on_indexed(self, result: IndexResult) -> None:
        self.indexed_events.append(result)

    def on_removed(self, resource_id: str, project: str) -> None:
        self.removed_events.append((resource_id, project))

    def on_error(self, resource_id: str, error: str) -> None:
        self.error_events.append((resource_id, error))


# ---------------------------------------------------------------------------
# Failing backends (test doubles)
# ---------------------------------------------------------------------------


class FailingGraphBackend:
    """Graph backend that raises on add_triple."""

    def add_triple(
        self,
        project: str,
        subject: str,
        predicate: str,
        obj: str,
    ) -> None:
        raise RuntimeError("Simulated graph failure")

    def remove_triples(
        self,
        project: str,
        subject: str | None,
        predicate: str | None,
        obj: str | None,
    ) -> None:
        pass

    def query(self, project: str, sparql: str) -> list[dict[str, str]]:
        return []

    def triple_count(self, project: str | None = None) -> int:
        return 0


class FailingTextBackend:
    """Text backend that raises on index_document."""

    def index_document(
        self,
        project: str,
        doc_id: str,
        content: str,
        metadata: dict[str, str],
    ) -> IndexedDocument:
        raise RuntimeError("Simulated text failure")

    def search(
        self,
        project: str,
        query: str,
        limit: int = 20,
    ) -> list[SearchResult]:
        return []

    def remove_document(self, project: str, doc_id: str) -> None:
        pass

    def rebuild_index(self, project: str) -> None:
        pass

    @property
    def document_count(self) -> int:
        return 0


class FailingVectorBackend:
    """Vector backend that raises on index_embedding."""

    def index_embedding(
        self,
        project: str,
        doc_id: str,
        embedding: list[float],
        metadata: dict[str, str],
    ) -> None:
        raise RuntimeError("Simulated vector failure")

    def search_similar(
        self,
        project: str,
        query_embedding: list[float],
        limit: int = 20,
        min_relevance: float = 0.0,
    ) -> list[SearchResult]:
        return []

    def remove_embedding(self, project: str, doc_id: str) -> None:
        pass

    @property
    def embedding_count(self) -> int:
        return 0


# ---------------------------------------------------------------------------
# Event bus stubs
# ---------------------------------------------------------------------------


class TrackingEventBus:
    """Minimal EventBus stub that records emitted events."""

    def __init__(self) -> None:
        self.events: list[DomainEvent] = []
        self._event = threading.Event()

    def emit(self, event: DomainEvent) -> None:
        self.events.append(event)
        self._event.set()

    def subscribe(
        self,
        event_type: EventType,
        handler: Callable[..., Any],
    ) -> None:
        _ = event_type, handler  # Not needed for tests

    def wait(self, timeout: float = 2.0) -> bool:
        return self._event.wait(timeout=timeout)


# ---------------------------------------------------------------------------
# Backends that fail on removal (not on add)
# ---------------------------------------------------------------------------


class RemovalFailingGraphBackend:
    """Graph backend that works for add but fails on remove_triples."""

    def __init__(self) -> None:
        self._triples: list[tuple[str, str, str, str]] = []

    def add_triple(self, project: str, subject: str, predicate: str, obj: str) -> None:
        self._triples.append((project, subject, predicate, obj))

    def remove_triples(
        self, project: str, subject: str | None, predicate: str | None, obj: str | None
    ) -> None:
        raise RuntimeError("Simulated graph removal failure")

    def query(self, project: str, sparql: str) -> list[dict[str, str]]:
        return []

    def triple_count(self, project: str | None = None) -> int:
        return len(self._triples)


class RemovalFailingTextBackend:
    """Text backend that works for add but fails on remove_document."""

    def index_document(
        self, project: str, doc_id: str, content: str, metadata: dict[str, str]
    ) -> IndexedDocument:
        return IndexedDocument(project=project, doc_id=doc_id, char_count=len(content))

    def search(self, project: str, query: str, limit: int = 20) -> list[SearchResult]:
        return []

    def remove_document(self, project: str, doc_id: str) -> None:
        raise RuntimeError("Simulated text removal failure")

    def rebuild_index(self, project: str) -> None:
        pass

    @property
    def document_count(self) -> int:
        return 0


class RemovalFailingVectorBackend:
    """Vector backend that works for add but fails on remove_embedding."""

    def index_embedding(
        self,
        project: str,
        doc_id: str,
        embedding: list[float],
        metadata: dict[str, str],
    ) -> None:
        pass

    def search_similar(
        self,
        project: str,
        query_embedding: list[float],
        limit: int = 20,
        min_relevance: float = 0.0,
    ) -> list[SearchResult]:
        return []

    def remove_embedding(self, project: str, doc_id: str) -> None:
        raise RuntimeError("Simulated vector removal failure")

    @property
    def embedding_count(self) -> int:
        return 0


class SelectiveFailingGraphBackend:
    """Graph backend that fails only on specific predicates."""

    def __init__(self, fail_on: set[str] | None = None) -> None:
        self._triples: list[tuple[str, str, str, str]] = []
        self._fail_on = fail_on or set()

    def add_triple(self, project: str, subject: str, predicate: str, obj: str) -> None:
        if predicate in self._fail_on:
            raise RuntimeError(f"Simulated failure on {predicate}")
        self._triples.append((project, subject, predicate, obj))

    def remove_triples(
        self, project: str, subject: str | None, predicate: str | None, obj: str | None
    ) -> None:
        pass

    def query(self, project: str, sparql: str) -> list[dict[str, str]]:
        return []

    def triple_count(self, project: str | None = None) -> int:
        return len(self._triples)
