"""
E2E tests for Interactive Parameter Resolution.

TASK-055-7: Tests the complete parameter resolution flow:
1. set_goal matches workflow and resolves parameters
2. YAML modifiers are resolved (tier 1)
3. Learned mappings are used (tier 2)
4. Unresolved parameters are flagged for LLM input (tier 3)
5. resolved_params stores values for future use

TASK-055-FIX: Updated for unified router_set_goal interface.

These tests run with mock router (no Blender connection required).
"""

from typing import Any, Dict, List, Optional

import pytest
from server.router.application.resolver.parameter_resolver import ParameterResolver
from server.router.application.resolver.parameter_store import ParameterStore
from server.router.domain.entities.parameter import (
    ParameterSchema,
)
from server.router.domain.interfaces.i_vector_store import (
    IVectorStore,
    SearchResult,
    VectorNamespace,
    VectorRecord,
    WeightedSearchResult,
)


class MockVectorStore(IVectorStore):
    """Mock vector store for testing parameter storage."""

    def __init__(self):
        self._records: Dict[str, VectorRecord] = {}

    def upsert(self, records: List[VectorRecord]) -> int:
        """Insert or update records."""
        for record in records:
            key = f"{record.namespace.value}:{record.id}"
            self._records[key] = record
        return len(records)

    def search(
        self,
        query_vector: List[float],
        namespace: VectorNamespace,
        top_k: int = 5,
        threshold: float = 0.0,
        metadata_filter: Optional[Dict[str, Any]] = None,
    ) -> List[SearchResult]:
        """Search for similar vectors."""
        results = []

        for key, record in self._records.items():
            if record.namespace != namespace:
                continue

            # Apply metadata filter
            if metadata_filter:
                match = True
                for k, v in metadata_filter.items():
                    if record.metadata.get(k) != v:
                        match = False
                        break
                if not match:
                    continue

            # Mock similarity - high score for testing
            score = 0.9

            if score >= threshold:
                results.append(
                    SearchResult(
                        id=record.id,
                        score=score,
                        text=record.text,
                        metadata=record.metadata,
                    )
                )

        return results[:top_k]

    def delete(self, ids: List[str], namespace: VectorNamespace) -> int:
        """Delete records by IDs."""
        count = 0
        for id_ in ids:
            key = f"{namespace.value}:{id_}"
            if key in self._records:
                del self._records[key]
                count += 1
        return count

    def count(self, namespace: Optional[VectorNamespace] = None) -> int:
        """Count records in namespace."""
        if namespace is None:
            return len(self._records)
        return sum(1 for r in self._records.values() if r.namespace == namespace)

    def list_ids(self, namespace: VectorNamespace) -> List[str]:
        """List all record IDs in namespace."""
        return [r.id for r in self._records.values() if r.namespace == namespace]

    def get_stats(self) -> Dict[str, Any]:
        """Get storage statistics."""
        return {
            "total_records": len(self._records),
            "type": "mock",
        }

    def rebuild_index(self) -> bool:
        """Rebuild the search index."""
        return True

    def clear(self, namespace: Optional[VectorNamespace] = None) -> int:
        """Clear all records from namespace."""
        if namespace is None:
            count = len(self._records)
            self._records.clear()
            return count
        keys_to_delete = [k for k, r in self._records.items() if r.namespace == namespace]
        for key in keys_to_delete:
            del self._records[key]
        return len(keys_to_delete)

    def search_workflows_weighted(
        self,
        query_vector: List[float],
        query_language: str = "en",
        top_k: int = 5,
        min_score: float = 0.5,
    ) -> List[WeightedSearchResult]:
        """Search workflows with weighted scoring."""
        # Mock implementation - not used for parameter tests
        return []

    def get_workflow_embedding_count(self) -> int:
        """Get count of workflow embeddings."""
        return self.count(VectorNamespace.WORKFLOWS)

    def get_unique_workflow_count(self) -> int:
        """Get count of unique workflows."""
        workflow_ids = set()
        for record in self._records.values():
            if record.namespace == VectorNamespace.WORKFLOWS:
                # Extract workflow_id from metadata if available
                workflow_id = record.metadata.get("workflow_id", record.id)
                workflow_ids.add(workflow_id)
        return len(workflow_ids)

    def get_all_ids(self, namespace: VectorNamespace) -> List[str]:
        """Get all record IDs in a namespace.

        Note: This is not part of IVectorStore interface but is used by
        ParameterStore.list_mappings().
        """
        return [r.id for r in self._records.values() if r.namespace == namespace]


class MockWorkflowClassifier:
    """Mock classifier implementing IWorkflowIntentClassifier for testing.

    Provides simple keyword-based similarity for testing parameter resolution
    without loading the full LaBSE model.
    """

    # Translation/similarity pairs for testing
    _related_pairs = [
        ("straight", "proste", 0.92),
        ("straight", "prosto", 0.90),
        ("legs", "nogi", 0.93),
        ("leg", "nogi", 0.91),
        ("wide", "szerokie", 0.91),
        ("narrow", "wąskie", 0.89),
        ("angle", "kąt", 0.88),
        ("tilt", "pochylenie", 0.87),
        ("straight", "gerade", 0.90),  # German
        ("legs", "beine", 0.92),
    ]

    def get_embedding(self, text: str) -> List[float]:
        """Get fake embedding for text."""
        # Generate a simple hash-based embedding
        import hashlib

        h = hashlib.md5(text.lower().encode()).digest()
        # Create 768-dim vector from hash
        embedding = []
        for i in range(768):
            embedding.append((h[i % 16] / 255.0) * 2 - 1)
        return embedding

    def similarity(self, text1: str, text2: str) -> float:
        """Calculate similarity based on keyword matching."""
        text1_lower = text1.lower()
        text2_lower = text2.lower()

        # Check for known translations/relations
        for t1, t2, score in self._related_pairs:
            if t1 in text1_lower and t2 in text2_lower:
                return score
            if t2 in text1_lower and t1 in text2_lower:
                return score

        # Basic word overlap for generic similarity
        words1 = set(text1_lower.split())
        words2 = set(text2_lower.split())
        overlap = len(words1 & words2)
        total = len(words1 | words2)
        if total == 0:
            return 0.0
        return overlap / total * 0.7  # Scale to max 0.7 for generic

    # Other required interface methods
    def load_workflow_embeddings(self, workflows):
        pass

    def find_similar(self, prompt, top_k=3, threshold=0.0):
        return []

    def find_best_match(self, prompt, min_confidence=0.5):
        return None

    def get_generalization_candidates(self, prompt, min_similarity=0.3, max_candidates=3):
        return []

    def is_loaded(self):
        return True

    def get_info(self):
        return {"type": "mock", "status": "loaded"}

    def clear_cache(self):
        return True


class TestInteractiveParameterResolutionE2E:
    """E2E tests for the interactive parameter resolution flow."""

    @pytest.fixture(scope="class")
    def mock_classifier(self):
        """Create mock classifier."""
        return MockWorkflowClassifier()

    @pytest.fixture(scope="class")
    def mock_vector_store(self):
        """Create mock vector store."""
        return MockVectorStore()

    @pytest.fixture(scope="class")
    def mock_store(self, mock_classifier, mock_vector_store):
        """Create parameter store with mock dependencies."""
        return ParameterStore(
            vector_store=mock_vector_store,
            classifier=mock_classifier,
        )

    @pytest.fixture(scope="class")
    def mock_resolver(self, mock_store, mock_classifier):
        """Create parameter resolver with mock store."""
        return ParameterResolver(
            classifier=mock_classifier,
            store=mock_store,
        )

    @pytest.fixture
    def table_workflow_params(self) -> Dict[str, ParameterSchema]:
        """Parameter schemas for picnic table workflow."""
        return {
            "leg_angle_left": ParameterSchema(
                name="leg_angle_left",
                type="float",
                range=(-1.57, 1.57),
                default=0.3,
                description="Left leg angle in radians",
                semantic_hints=["angle", "tilt", "slant", "nogi", "kąt"],
            ),
            "leg_angle_right": ParameterSchema(
                name="leg_angle_right",
                type="float",
                range=(-1.57, 1.57),
                default=0.3,
                description="Right leg angle in radians",
                semantic_hints=["angle", "tilt", "slant", "nogi", "kąt"],
            ),
            "top_width": ParameterSchema(
                name="top_width",
                type="float",
                range=(0.5, 3.0),
                default=1.5,
                description="Width of table top in meters",
                semantic_hints=["width", "szerokość", "wide", "narrow"],
            ),
        }

    def test_tier1_yaml_modifiers_resolved(self, mock_resolver, table_workflow_params):
        """Test that YAML modifiers (tier 1) are resolved first."""
        existing_modifiers = {
            "leg_angle_left": 0.0,
            "leg_angle_right": 0.0,
        }

        result = mock_resolver.resolve(
            prompt="picnic table with straight legs",
            workflow_name="picnic_table",
            parameters=table_workflow_params,
            existing_modifiers=existing_modifiers,
        )

        # YAML modifiers should be resolved
        assert "leg_angle_left" in result.resolved
        assert result.resolved["leg_angle_left"] == 0.0
        assert result.resolution_sources["leg_angle_left"] == "yaml_modifier"

        assert "leg_angle_right" in result.resolved
        assert result.resolved["leg_angle_right"] == 0.0
        assert result.resolution_sources["leg_angle_right"] == "yaml_modifier"

        # top_width not mentioned in prompt + not in modifiers = gets default
        # (only parameters with high relevance but no value go to unresolved)
        assert "top_width" in result.resolved
        assert result.resolution_sources["top_width"] == "default"

    def test_tier2_learned_mapping_used(self, mock_resolver, mock_store, table_workflow_params):
        """Test that learned mappings (tier 2) are used when no YAML modifier."""
        # First, store a learned mapping
        mock_store.store_mapping(
            context="straight legs",
            parameter_name="leg_angle_left",
            value=0.0,
            workflow_name="picnic_table",
        )
        mock_store.store_mapping(
            context="straight legs",
            parameter_name="leg_angle_right",
            value=0.0,
            workflow_name="picnic_table",
        )

        # Resolve without YAML modifiers
        result = mock_resolver.resolve(
            prompt="table with straight legs",  # Similar to stored context
            workflow_name="picnic_table",
            parameters=table_workflow_params,
            existing_modifiers={},  # No YAML modifiers
        )

        # Should use learned mapping
        # Note: depends on classifier similarity score
        [k for k, v in result.resolution_sources.items() if v == "learned_mapping"]
        # At minimum, should have some resolution happening
        assert len(result.resolved) > 0 or len(result.unresolved) > 0

    def test_tier3_unresolved_for_llm(self, mock_resolver, table_workflow_params):
        """Test that unresolved parameters are flagged for LLM input."""

        # Clear any stored mappings by using a fresh resolver
        class LowSimilarityClassifier:
            """Classifier that returns low similarity - nothing matches."""

            def get_embedding(self, text: str) -> List[float]:
                return [0.5] * 768

            def similarity(self, text1: str, text2: str) -> float:
                return 0.3  # Low similarity - nothing matches

        fresh_vector_store = MockVectorStore()
        fresh_classifier = LowSimilarityClassifier()
        fresh_store = ParameterStore(
            vector_store=fresh_vector_store,
            classifier=fresh_classifier,
        )
        fresh_resolver = ParameterResolver(
            classifier=fresh_classifier,
            store=fresh_store,
        )

        result = fresh_resolver.resolve(
            prompt="table",  # Vague prompt, no modifiers
            workflow_name="picnic_table",
            parameters=table_workflow_params,
            existing_modifiers={},
        )

        # Parameters should be unresolved or use defaults
        # With low similarity, prompt doesn't relate to params
        # So parameters go to default, not unresolved
        total_items = len(result.resolved) + len(result.unresolved)
        assert total_items == len(table_workflow_params)

    def test_store_resolved_value_persists(self, mock_resolver, mock_store, table_workflow_params):
        """Test that storing a resolved value makes it available for future use."""
        # Store a value
        result = mock_resolver.store_resolved_value(
            context="prostymi nogami",  # Polish for "straight legs"
            parameter_name="leg_angle_left",
            value=0.0,
            workflow_name="picnic_table",
            schema=table_workflow_params["leg_angle_left"],
        )

        assert "Stored" in result

        # Verify it was persisted by trying to find it
        mapping = mock_store.find_mapping(
            prompt="prostymi nogami",
            workflow_name="picnic_table",
            parameter_name="leg_angle_left",
        )
        assert mapping is not None, "Stored mapping should be findable"
        assert mapping.value == 0.0

    def test_store_invalid_value_rejected(self, mock_resolver, table_workflow_params):
        """Test that invalid values are rejected during storage."""
        result = mock_resolver.store_resolved_value(
            context="extreme angle",
            parameter_name="leg_angle_left",
            value=5.0,  # Way outside range (-1.57, 1.57)
            workflow_name="picnic_table",
            schema=table_workflow_params["leg_angle_left"],
        )

        assert "Error" in result or "invalid" in result.lower()

    def test_complete_interactive_flow(self, mock_resolver, mock_store, table_workflow_params):
        """Test the complete interactive resolution flow."""
        # Step 1: First request with some YAML modifiers
        modifiers = {"leg_angle_left": 0.0}

        result1 = mock_resolver.resolve(
            prompt="picnic table with straight left leg",
            workflow_name="picnic_table",
            parameters=table_workflow_params,
            existing_modifiers=modifiers,
        )

        # left leg resolved by YAML
        assert result1.resolved.get("leg_angle_left") == 0.0
        assert result1.resolution_sources.get("leg_angle_left") == "yaml_modifier"

        # Other params unresolved or default
        assert not result1.is_complete or len(result1.unresolved) == 0

        # Step 2: Store remaining values for future
        mock_resolver.store_resolved_value(
            context="straight right leg",
            parameter_name="leg_angle_right",
            value=0.0,
            workflow_name="picnic_table",
            schema=table_workflow_params["leg_angle_right"],
        )

        # Step 3: Future request without YAML should use learned
        result2 = mock_resolver.resolve(
            prompt="table with straight right leg",
            workflow_name="picnic_table",
            parameters={"leg_angle_right": table_workflow_params["leg_angle_right"]},
            existing_modifiers={},
        )

        # Should find the learned mapping
        # (depends on similarity threshold)
        assert len(result2.resolved) + len(result2.unresolved) == 1


class TestMultilingualParameterResolution:
    """Test parameter resolution works across languages (via LaBSE)."""

    @pytest.fixture
    def multilingual_classifier(self):
        """Classifier that simulates LaBSE cross-lingual matching."""
        return MockWorkflowClassifier()

    @pytest.fixture
    def multilingual_vector_store(self):
        """Vector store for multilingual tests."""
        return MockVectorStore()

    @pytest.fixture
    def multilingual_store(self, multilingual_classifier, multilingual_vector_store):
        """Store with multilingual classifier."""
        return ParameterStore(
            vector_store=multilingual_vector_store,
            classifier=multilingual_classifier,
        )

    def test_polish_prompt_matches_english_stored(self, multilingual_store):
        """Test that Polish prompt finds English-stored mapping."""
        # Store with English context
        multilingual_store.store_mapping(
            context="straight legs",
            parameter_name="leg_angle",
            value=0.0,
            workflow_name="table",
        )

        # Find with Polish context - uses similarity()
        mapping = multilingual_store.find_mapping(
            prompt="proste nogi",  # Polish
            workflow_name="table",
            parameter_name="leg_angle",
            similarity_threshold=0.85,
        )

        # Should find due to semantic similarity (straight->proste, legs->nogi)
        assert mapping is not None
        assert mapping.value == 0.0

    def test_german_prompt_matches_english_stored(self, multilingual_store):
        """Test that German prompt finds English-stored mapping."""
        multilingual_store.store_mapping(
            context="straight legs",
            parameter_name="leg_angle",
            value=0.0,
            workflow_name="table",
        )

        # Find with German context
        mapping = multilingual_store.find_mapping(
            prompt="gerade beine",  # German
            workflow_name="table",
            parameter_name="leg_angle",
            similarity_threshold=0.85,
        )

        # Should find due to semantic similarity
        assert mapping is not None
        assert mapping.value == 0.0


class TestParameterResolutionEdgeCases:
    """Edge case tests for parameter resolution."""

    @pytest.fixture
    def edge_classifier(self):
        """Create classifier for edge case tests."""
        return MockWorkflowClassifier()

    @pytest.fixture
    def edge_vector_store(self):
        """Create vector store for edge case tests."""
        return MockVectorStore()

    def test_empty_parameters_returns_complete(self, edge_classifier, edge_vector_store):
        """Test that empty parameters dict returns complete result."""
        store = ParameterStore(
            vector_store=edge_vector_store,
            classifier=edge_classifier,
        )
        resolver = ParameterResolver(classifier=edge_classifier, store=store)

        result = resolver.resolve(
            prompt="some goal",
            workflow_name="test",
            parameters={},
            existing_modifiers={},
        )

        assert result.is_complete is True
        assert result.needs_llm_input is False
        assert len(result.resolved) == 0
        assert len(result.unresolved) == 0

    def test_all_params_from_modifiers(self, edge_classifier, edge_vector_store):
        """Test when all parameters come from YAML modifiers."""
        store = ParameterStore(
            vector_store=edge_vector_store,
            classifier=edge_classifier,
        )
        resolver = ParameterResolver(classifier=edge_classifier, store=store)

        params = {
            "param1": ParameterSchema(name="param1", type="float"),
            "param2": ParameterSchema(name="param2", type="int"),
        }

        result = resolver.resolve(
            prompt="test",
            workflow_name="test",
            parameters=params,
            existing_modifiers={"param1": 1.0, "param2": 5},
        )

        assert result.is_complete is True
        assert result.resolved == {"param1": 1.0, "param2": 5}
        assert len(result.unresolved) == 0

    def test_none_existing_modifiers_treated_as_empty(self, edge_classifier, edge_vector_store):
        """Test that None existing_modifiers is treated as empty dict."""
        store = ParameterStore(
            vector_store=edge_vector_store,
            classifier=edge_classifier,
        )
        resolver = ParameterResolver(classifier=edge_classifier, store=store)

        params = {
            "param1": ParameterSchema(name="param1", type="float", default=1.0),
        }

        # Should not crash with None
        result = resolver.resolve(
            prompt="test",
            workflow_name="test",
            parameters=params,
            existing_modifiers=None,
        )

        # Should have processed the parameter
        assert len(result.resolved) + len(result.unresolved) == 1
