from __future__ import annotations

"""
Copyright (C) 2025 Luciano Guerche

This file is part of rabbitmq-mcp-server.

rabbitmq-mcp-server is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

rabbitmq-mcp-server is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with rabbitmq-mcp-server. If not, see <https://www.gnu.org/licenses/>.
"""

"""Unit tests for search_ids tool implementation."""

import numpy as np
import pytest

from tools.search_ids import _get_cached_query_embedding, search_ids


@pytest.fixture
def sample_embeddings() -> dict:
    """Sample embeddings with known vectors."""
    return {
        "model_name": "sentence-transformers/all-MiniLM-L6-v2",
        "embedding_dimension": 384,
        "embeddings": {
            "queues.list": [1.0] + [0.0] * 383,  # Unit vector along first dimension
            "queues.create": [0.9, 0.1] + [0.0] * 382,
            "exchanges.list": [0.0, 1.0] + [0.0] * 382,  # Orthogonal to queues.list
            "bindings.list": [0.5, 0.5] + [0.0] * 382,
        },
    }


@pytest.fixture
def sample_operations() -> dict:
    """Sample operations registry."""
    return {
        "total_operations": 4,
        "operations": {
            "queues.list": {
                "operation_id": "queues.list",
                "namespace": "queues",
                "description": "List all queues",
            },
            "queues.create": {
                "operation_id": "queues.create",
                "namespace": "queues",
                "description": "Create a new queue",
            },
            "exchanges.list": {
                "operation_id": "exchanges.list",
                "namespace": "exchanges",
                "description": "List all exchanges",
            },
            "bindings.list": {
                "operation_id": "bindings.list",
                "namespace": "bindings",
                "description": "List all bindings",
            },
        },
    }


def test_query_embedding_generation_dimensions(mocker):
    """Test AC#1: Query embedding produces 384D vector."""
    mock_embed = mocker.patch("tools.search_ids.embed_text")
    mock_embed.return_value = np.random.rand(384).astype(np.float32)

    _get_cached_query_embedding.cache_clear()
    embedding = _get_cached_query_embedding("test query")

    assert len(embedding) == 384
    assert isinstance(embedding, tuple)


def test_cosine_similarity_identical_vectors(sample_embeddings, sample_operations):
    """Test AC#1: Cosine similarity with identical vectors = 1.0."""
    # Mock embed_text to return same vector as queues.list
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    tools.search_ids.embed_text = lambda x: np.array(
        [1.0] + [0.0] * 383, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test", embeddings=sample_embeddings, operations_registry=sample_operations
    )

    tools.search_ids.embed_text = original_embed

    # queues.list should have highest similarity (should be 1.0)
    assert len(result.results) > 0
    assert result.results[0].operation_id == "queues.list"
    assert result.results[0].similarity_score == pytest.approx(1.0, abs=0.01)


def test_cosine_similarity_orthogonal_vectors(sample_embeddings, sample_operations):
    """Test AC#1: Cosine similarity with orthogonal vectors = 0.0."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    # Query vector orthogonal to queues.list
    tools.search_ids.embed_text = lambda x: np.array(
        [0.0, 1.0] + [0.0] * 382, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test",
        threshold=0.0,
        embeddings=sample_embeddings,
        operations_registry=sample_operations,
    )

    tools.search_ids.embed_text = original_embed

    # exchanges.list should have highest score
    assert result.results[0].operation_id == "exchanges.list"


def test_threshold_filtering(sample_embeddings, sample_operations):
    """Test AC#2: Results with scores < threshold are excluded."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    tools.search_ids.embed_text = lambda x: np.array(
        [1.0] + [0.0] * 383, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test",
        threshold=0.95,
        embeddings=sample_embeddings,
        operations_registry=sample_operations,
    )

    tools.search_ids.embed_text = original_embed

    # Only queues.list should pass threshold of 0.95
    assert all(r.similarity_score >= 0.95 for r in result.results)


def test_result_sorting_descending(sample_embeddings, sample_operations):
    """Test AC#2: Results sorted by similarity score descending."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    tools.search_ids.embed_text = lambda x: np.array(
        [0.7, 0.3] + [0.0] * 382, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test",
        threshold=0.0,
        embeddings=sample_embeddings,
        operations_registry=sample_operations,
    )

    tools.search_ids.embed_text = original_embed

    # Verify scores are descending
    scores = [r.similarity_score for r in result.results]
    assert scores == sorted(scores, reverse=True)


def test_result_format(sample_embeddings, sample_operations):
    """Test AC#2: Each result contains required fields."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    tools.search_ids.embed_text = lambda x: np.array(
        [1.0] + [0.0] * 383, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test", embeddings=sample_embeddings, operations_registry=sample_operations
    )

    tools.search_ids.embed_text = original_embed

    for match in result.results:
        assert hasattr(match, "operation_id")
        assert hasattr(match, "description")
        assert hasattr(match, "similarity_score")
        assert hasattr(match, "namespace")
        assert isinstance(match.operation_id, str)
        assert isinstance(match.description, str)
        assert isinstance(match.similarity_score, float)
        assert isinstance(match.namespace, str)


def test_max_results_limit(sample_embeddings, sample_operations):
    """Test AC#3: Results limited to max_results parameter."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    tools.search_ids.embed_text = lambda x: np.array(
        [0.5, 0.5] + [0.0] * 382, dtype=np.float32
    )

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test",
        threshold=0.0,
        max_results=2,
        embeddings=sample_embeddings,
        operations_registry=sample_operations,
    )

    tools.search_ids.embed_text = original_embed

    assert len(result.results) <= 2


def test_max_results_cap_at_50(sample_embeddings, sample_operations):
    """Test AC#3: max_results exceeding 50 raises error."""
    with pytest.raises(ValueError, match="max_results must be between 1 and 50"):
        search_ids(
            "test",
            max_results=51,
            embeddings=sample_embeddings,
            operations_registry=sample_operations,
        )


def test_empty_query_error(sample_embeddings, sample_operations):
    """Test AC#6: Empty query returns error."""
    with pytest.raises(ValueError, match="query must be a non-empty string"):
        search_ids(
            "", embeddings=sample_embeddings, operations_registry=sample_operations
        )


def test_invalid_threshold_too_low(sample_embeddings, sample_operations):
    """Test AC#6: Threshold < 0.0 returns error."""
    with pytest.raises(ValueError, match="threshold must be between 0.0 and 1.0"):
        search_ids(
            "test",
            threshold=-0.1,
            embeddings=sample_embeddings,
            operations_registry=sample_operations,
        )


def test_invalid_threshold_too_high(sample_embeddings, sample_operations):
    """Test AC#6: Threshold > 1.0 returns error."""
    with pytest.raises(ValueError, match="threshold must be between 0.0 and 1.0"):
        search_ids(
            "test",
            threshold=1.1,
            embeddings=sample_embeddings,
            operations_registry=sample_operations,
        )


def test_invalid_max_results_too_low(sample_embeddings, sample_operations):
    """Test AC#6: max_results < 1 returns error."""
    with pytest.raises(ValueError, match="max_results must be between 1 and 50"):
        search_ids(
            "test",
            max_results=0,
            embeddings=sample_embeddings,
            operations_registry=sample_operations,
        )


def test_invalid_max_results_too_high(sample_embeddings, sample_operations):
    """Test AC#6: max_results > 50 returns error."""
    with pytest.raises(ValueError, match="max_results must be between 1 and 50"):
        search_ids(
            "test",
            max_results=51,
            embeddings=sample_embeddings,
            operations_registry=sample_operations,
        )


def test_empty_results_handling(sample_embeddings, sample_operations):
    """Test AC#2: Empty results when all scores < threshold."""
    import tools.search_ids

    original_embed = tools.search_ids.embed_text
    # Use vector that won't match anything with high threshold
    tools.search_ids.embed_text = lambda x: np.array([0.0] * 384, dtype=np.float32)

    _get_cached_query_embedding.cache_clear()
    result = search_ids(
        "test",
        threshold=0.99,
        embeddings=sample_embeddings,
        operations_registry=sample_operations,
    )

    tools.search_ids.embed_text = original_embed

    assert len(result.results) == 0
    assert isinstance(result.results, list)


def test_lru_cache_behavior():
    """Test AC#4: LRU cache hit/miss behavior."""
    _get_cached_query_embedding.cache_clear()

    # First call - cache miss
    cache_info_before = _get_cached_query_embedding.cache_info()
    _get_cached_query_embedding("test query 1")
    cache_info_after = _get_cached_query_embedding.cache_info()

    assert cache_info_after.misses == cache_info_before.misses + 1

    # Second call with same query - cache hit
    cache_info_before = _get_cached_query_embedding.cache_info()
    _get_cached_query_embedding("test query 1")
    cache_info_after = _get_cached_query_embedding.cache_info()

    assert cache_info_after.hits == cache_info_before.hits + 1


def test_embeddings_not_loaded_error(sample_operations):
    """Test AC#6: Error when embeddings not loaded."""
    with pytest.raises(ValueError, match="embeddings data not loaded"):
        search_ids("test", embeddings=None, operations_registry=sample_operations)


def test_operations_not_loaded_error(sample_embeddings):
    """Test AC#6: Error when operations_registry not loaded."""
    with pytest.raises(ValueError, match="operations_registry data not loaded"):
        search_ids("test", embeddings=sample_embeddings, operations_registry=None)
