"""Step definitions for ``features/semantic_chunking_strategy.feature``.

Covers the SemanticChunkingStrategy:

* Construction and protocol compliance
* Configuration parameters (embedding_model, top_k)
* Core assembly behaviour (cosine similarity ranking)
* Embedding caching
* Plugin registry registration
"""

from __future__ import annotations

import math
from typing import Any

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

from cleveragents.application.services.acms_service import ACMSPipeline
from cleveragents.application.services.semantic_chunking_strategy import (
    SemanticChunkingStrategy,
)
from cleveragents.domain.models.core.context_fragment import (
    ContextBudget,
    ContextFragment,
    FragmentProvenance,
)

__all__: list[str] = []


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


def _make_sc_fragment(
    uko_node: str,
    content: str,
    score: float,
    tokens: int,
    depth: int,
) -> ContextFragment:
    """Build a ``ContextFragment`` for semantic chunking test purposes."""
    return ContextFragment(
        uko_node=uko_node,
        content=content,
        relevance_score=score,
        token_count=tokens,
        detail_depth=depth,
        provenance=FragmentProvenance(resource_uri=uko_node),
    )


def _word_based_embedding(text: str) -> list[float]:
    """Word-based mock embedding for deterministic similarity testing.

    Returns a 64-element vector where each element corresponds to a word
    in a fixed vocabulary.  This ensures that texts sharing words have
    higher cosine similarity.
    """
    # Fixed vocabulary of 64 words for testing
    vocab = [
        "database",
        "connection",
        "pool",
        "manager",
        "sql",
        "query",
        "executor",
        "file",
        "input",
        "output",
        "handler",
        "alpha",
        "beta",
        "gamma",
        "delta",
        "epsilon",
        "zeta",
        "eta",
        "theta",
        "iota",
        "kappa",
        "lambda",
        "mu",
        "hello",
        "world",
        "there",
        "again",
        "test",
        "content",
        "same",
        "project",
        "app",
        "py",
        "data",
        "model",
        "service",
        "api",
        "client",
        "server",
        "cache",
        "index",
        "search",
        "vector",
        "embed",
        "chunk",
        "semantic",
        "context",
        "fragment",
        "budget",
        "token",
        "score",
        "rank",
        "sort",
        "filter",
        "select",
        "top",
        "anchor",
        "query",
        "text",
        "word",
        "char",
        "string",
        "list",
    ]
    words = text.lower().split()
    vec = [0.0] * 64
    for word in words:
        if word in vocab:
            idx = vocab.index(word)
            vec[idx] += 1.0
    magnitude = math.sqrt(sum(v * v for v in vec))
    if magnitude > 0.0:
        vec = [v / magnitude for v in vec]
    return vec


# ---------------------------------------------------------------------------
# Given steps — construction
# ---------------------------------------------------------------------------


@given("a SemanticChunkingStrategy with default parameters")
def step_semantic_chunking_default(context: Context) -> None:
    context.sc_strategy = SemanticChunkingStrategy()


@given('a SemanticChunkingStrategy with embedding_model "{model}"')
def step_semantic_chunking_with_model(context: Context, model: str) -> None:
    context.sc_strategy = SemanticChunkingStrategy(embedding_model=model)


@given("a SemanticChunkingStrategy with top_k {top_k:d}")
def step_semantic_chunking_with_top_k(context: Context, top_k: int) -> None:
    context.sc_strategy = SemanticChunkingStrategy(top_k=top_k)


@given("a SemanticChunkingStrategy with mock embeddings")
def step_semantic_chunking_with_mock_embeddings(context: Context) -> None:
    context.sc_strategy = SemanticChunkingStrategy(
        embedding_fn=_word_based_embedding,
    )


@given("a SemanticChunkingStrategy with top_k {top_k:d} and mock embeddings")
def step_semantic_chunking_top_k_mock(context: Context, top_k: int) -> None:
    context.sc_strategy = SemanticChunkingStrategy(
        top_k=top_k,
        embedding_fn=_word_based_embedding,
    )


@given("a SemanticChunkingStrategy with call-counting mock embeddings")
def step_semantic_chunking_call_counting(context: Context) -> None:
    context.sc_call_count = 0

    def counting_embedding(text: str) -> list[float]:
        context.sc_call_count += 1
        return _word_based_embedding(text)

    context.sc_strategy = SemanticChunkingStrategy(
        embedding_fn=counting_embedding,
    )


# ---------------------------------------------------------------------------
# Given steps — fragments and budget
# ---------------------------------------------------------------------------


@given("an empty semantic chunking fragment list")
def step_sc_empty_fragments(context: Context) -> None:
    context.sc_fragments: list[ContextFragment] = []


@given("the following semantic chunking fragments:")
def step_sc_fragments_table(context: Context) -> None:
    context.sc_fragments = []
    for row in context.table:
        frag = _make_sc_fragment(
            uko_node=row["uko_node"],
            content=row["content"],
            score=float(row["score"]),
            tokens=int(row["tokens"]),
            depth=int(row["depth"]),
        )
        context.sc_fragments.append(frag)


@given(
    "a semantic chunking budget with max_tokens {max_tokens:d} and reserved_tokens {reserved:d}"
)
def step_sc_budget(context: Context, max_tokens: int, reserved: int) -> None:
    context.sc_budget = ContextBudget(max_tokens=max_tokens, reserved_tokens=reserved)


@given("an ACMS pipeline for semantic chunking tests")
def step_sc_pipeline(context: Context) -> None:
    context.sc_pipeline = ACMSPipeline()


@given("a valid plan ID for semantic chunking")
def step_sc_plan_id(context: Context) -> None:
    # Use a valid ULID
    context.sc_plan_id = "01ARZ3NDEKTSV4RRFFQ69G5FAV"


# ---------------------------------------------------------------------------
# When steps
# ---------------------------------------------------------------------------


@when('I check can_handle on SemanticChunkingStrategy with query "{query}"')
def step_sc_can_handle_with_query(context: Context, query: str) -> None:
    request: dict[str, Any] = {"query": query}
    context.sc_confidence = context.sc_strategy.can_handle(request)


@when("I check can_handle on SemanticChunkingStrategy without query")
def step_sc_can_handle_no_query(context: Context) -> None:
    request: dict[str, Any] = {}
    context.sc_confidence = context.sc_strategy.can_handle(request)


@when('I assemble with the SemanticChunkingStrategy with anchor "{anchor}"')
def step_sc_assemble_with_anchor(context: Context, anchor: str) -> None:
    context.sc_strategy.set_anchor(anchor)
    context.sc_result = list(
        context.sc_strategy.assemble(context.sc_fragments, context.sc_budget)
    )


@when("I assemble with the SemanticChunkingStrategy without anchor")
def step_sc_assemble_no_anchor(context: Context) -> None:
    # Do not set anchor — strategy should fall back to relevance ordering
    context.sc_result = list(
        context.sc_strategy.assemble(context.sc_fragments, context.sc_budget)
    )


@when('I assemble via the pipeline with strategy "semantic_chunking"')
def step_sc_pipeline_assemble(context: Context) -> None:
    context.sc_payload = context.sc_pipeline.assemble(
        plan_id=context.sc_plan_id,
        fragments=context.sc_fragments,
        budget=context.sc_budget,
        strategy="semantic_chunking",
    )


# ---------------------------------------------------------------------------
# Then steps
# ---------------------------------------------------------------------------


@then('the SemanticChunkingStrategy name should be "{name}"')
def step_sc_name(context: Context, name: str) -> None:
    actual = context.sc_strategy.name
    assert actual == name, f"Expected name '{name}', got '{actual}'"


@then("the SemanticChunkingStrategy should support semantic search")
def step_sc_supports_semantic(context: Context) -> None:
    caps = context.sc_strategy.capabilities
    assert caps.supports_semantic_search, (
        "SemanticChunkingStrategy should support semantic search"
    )


@then("the SemanticChunkingStrategy confidence should be greater than 0.0")
def step_sc_confidence_positive(context: Context) -> None:
    assert context.sc_confidence > 0.0, (
        f"Expected confidence > 0.0, got {context.sc_confidence}"
    )


@then("the SemanticChunkingStrategy confidence should be {expected:g}")
def step_sc_confidence_exact(context: Context, expected: float) -> None:
    actual = context.sc_confidence
    assert abs(actual - expected) < 1e-6, (
        f"Expected confidence {expected}, got {actual}"
    )


@then('the SemanticChunkingStrategy explain should contain "{text}"')
def step_sc_explain(context: Context, text: str) -> None:
    explanation = context.sc_strategy.explain()
    assert text.lower() in explanation.lower(), (
        f"Expected explain to contain '{text}', got: {explanation}"
    )


@then('the SemanticChunkingStrategy embedding_model should be "{model}"')
def step_sc_embedding_model(context: Context, model: str) -> None:
    actual = context.sc_strategy.embedding_model
    assert actual == model, f"Expected embedding_model '{model}', got '{actual}'"


@then("the SemanticChunkingStrategy top_k should be {expected:d}")
def step_sc_top_k(context: Context, expected: int) -> None:
    actual = context.sc_strategy.top_k
    assert actual == expected, f"Expected top_k {expected}, got {actual}"


@then("the semantic chunking result should contain {count:d} fragments")
def step_sc_fragment_count_exact(context: Context, count: int) -> None:
    actual = len(context.sc_result)
    assert actual == count, f"Expected {count} fragments, got {actual}"


@then("the semantic chunking result should contain at most {count:d} fragments")
def step_sc_fragment_count_at_most(context: Context, count: int) -> None:
    actual = len(context.sc_result)
    assert actual <= count, f"Expected at most {count} fragments, got {actual}"


@then('the first semantic chunking result should have uko_node "{uko_node}"')
def step_sc_first_result_uko_node(context: Context, uko_node: str) -> None:
    assert len(context.sc_result) > 0, "Expected at least one result fragment"
    actual = context.sc_result[0].uko_node
    assert actual == uko_node, (
        f"Expected first fragment uko_node '{uko_node}', got '{actual}'"
    )


@then(
    "the embedding model should be called fewer times than total fragments plus anchor"
)
def step_sc_cache_efficiency(context: Context) -> None:
    # With 2 fragments having identical content + 1 anchor = 3 unique texts
    # But "same content" appears twice, so only 2 unique texts (anchor + content)
    # The call count should be less than len(fragments) + 1 (anchor)
    total_possible = len(context.sc_fragments) + 1  # +1 for anchor
    actual_calls = context.sc_call_count
    assert actual_calls < total_possible, (
        f"Expected fewer than {total_possible} embedding calls due to caching, "
        f"got {actual_calls}"
    )


@then('the sc_pipeline should have strategy "semantic_chunking"')
def step_sc_pipeline_has_strategy(context: Context) -> None:
    registered = context.sc_pipeline._strategies
    assert "semantic_chunking" in registered, (
        f"Pipeline does not have strategy 'semantic_chunking'. "
        f"Registered: {list(registered.keys())}"
    )


@then("the pipeline payload should contain at least 1 fragment")
def step_sc_payload_has_fragments(context: Context) -> None:
    count = len(context.sc_payload.fragments)
    assert count >= 1, f"Expected at least 1 fragment in payload, got {count}"
