"""Unit tests for ai/intent_parser.py without ROS dependency.

Tests the intent parser logic using mocks to avoid ROS2 dependencies.
"""

import re
from unittest.mock import Mock, patch

import pytest


class TestIntentParserNoROS:
    """Test IntentParserNode logic without ROS dependencies."""

    @pytest.fixture
    def parser_class(self):
        """Get the IntentParserNode class with mocked ROS."""
        with patch.dict(
            "sys.modules",
            {
                "rclpy": Mock(),
                "rclpy.node": Mock(),
                "agent_ros_bridge_msgs.msg": Mock(),
                "agent_ros_bridge_msgs.srv": Mock(),
            },
        ):
            # Create mock for ROS messages
            mock_msgs = Mock()
            mock_msgs.Entity = Mock()
            mock_msgs.Intent = Mock()
            mock_srvs = Mock()
            mock_srvs.ParseIntent = Mock()
            mock_srvs.ParseIntent.Request = Mock
            mock_srvs.ParseIntent.Response = Mock

            sys_modules = {
                "rclpy": Mock(),
                "rclpy.node": Mock(),
                "agent_ros_bridge_msgs.msg": mock_msgs,
                "agent_ros_bridge_msgs.srv": mock_srvs,
            }

            with patch.dict("sys.modules", sys_modules):
                # Import the module
                import sys

                # Remove cached module if exists
                if "agent_ros_bridge.ai.intent_parser" in sys.modules:
                    del sys.modules["agent_ros_bridge.ai.intent_parser"]

                # Now import with mocked dependencies
                from agent_ros_bridge.ai.intent_parser import IntentParserNode

                return IntentParserNode

    def test_patterns_compiled_correctly(self):
        """Test that regex patterns are valid and compile."""
        # Define patterns directly from the source
        PATTERNS = {
            "NAVIGATE": [
                r"go\s+to\s+(?:the\s+)?(?P<location>\w+)",
                r"navigate\s+to\s+(?:the\s+)?(?P<location>\w+)",
                r"move\s+to\s+(?:the\s+)?(?P<location>\w+)",
                r"drive\s+to\s+(?:the\s+)?(?P<location>\w+)",
                r"head\s+to\s+(?:the\s+)?(?P<location>\w+)",
                r"proceed\s+to\s+(?:the\s+)?(?P<location>\w+)",
            ],
            "MANIPULATE": [
                r"pick\s+up\s+(?:the\s+)?(?P<object>\w+)",
                r"grab\s+(?:the\s+)?(?P<object>\w+)",
                r"place\s+(?:the\s+)?(?P<object>\w+)",
                r"put\s+down\s+(?:the\s+)?(?P<object>\w+)",
                r"drop\s+(?:the\s+)?(?P<object>\w+)",
            ],
            "SENSE": [
                r"what\s+do\s+you\s+see",
                r"scan\s+(?:the\s+)?(?P<area>\w+)",
                r"look\s+(?:at\s+)?(?:the\s+)?(?P<object>\w+)",
                r"detect\s+(?:the\s+)?(?P<object>\w+)",
                r"observe\s+(?:the\s+)?(?P<area>\w+)",
            ],
            "QUERY": [
                r"where\s+are\s+you",
                r"what\s+is\s+your\s+status",
                r"battery\s+level",
                r"what\s+is\s+your\s+position",
                r"report\s+status",
            ],
            "SAFETY": [
                r"^stop$",
                r"emergency\s+stop",
                r"^halt$",
                r"^freeze$",
                r"^abort$",
            ],
            "CONFIGURE": [
                r"set\s+speed\s+to\s+(?P<speed>\w+)",
                r"set\s+mode\s+to\s+(?P<mode>\w+)",
                r"enable\s+(?P<feature>\w+)",
                r"disable\s+(?P<feature>\w+)",
            ],
        }

        for intent_type, patterns in PATTERNS.items():
            for pattern in patterns:
                compiled = re.compile(pattern, re.IGNORECASE)
                assert compiled is not None, f"Failed to compile {intent_type} pattern: {pattern}"

    def test_navigate_patterns_match(self):
        """Test that navigate patterns match expected utterances."""
        patterns = [
            (r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", "go to kitchen", "kitchen"),
            (r"navigate\s+to\s+(?:the\s+)?(?P<location>\w+)", "navigate to office", "office"),
            (r"move\s+to\s+(?:the\s+)?(?P<location>\w+)", "move to bedroom", "bedroom"),
            (r"drive\s+to\s+(?:the\s+)?(?P<location>\w+)", "drive to garage", "garage"),
            (r"head\s+to\s+(?:the\s+)?(?P<location>\w+)", "head to lobby", "lobby"),
            (r"proceed\s+to\s+(?:the\s+)?(?P<location>\w+)", "proceed to exit", "exit"),
        ]

        for pattern, utterance, expected_location in patterns:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"
            assert match.group("location") == expected_location

    def test_manipulate_patterns_match(self):
        """Test that manipulate patterns match expected utterances."""
        patterns = [
            (r"pick\s+up\s+(?:the\s+)?(?P<object>\w+)", "pick up the cup", "cup"),
            (r"grab\s+(?:the\s+)?(?P<object>\w+)", "grab the box", "box"),
            (r"place\s+(?:the\s+)?(?P<object>\w+)", "place the book", "book"),
            (r"put\s+down\s+(?:the\s+)?(?P<object>\w+)", "put down the bottle", "bottle"),
            (r"drop\s+(?:the\s+)?(?P<object>\w+)", "drop the item", "item"),
        ]

        for pattern, utterance, expected_object in patterns:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"
            assert match.group("object") == expected_object

    def test_sense_patterns_match(self):
        """Test that sense patterns match expected utterances."""
        test_cases = [
            (r"what\s+do\s+you\s+see", "what do you see"),
            (r"scan\s+(?:the\s+)?(?P<area>\w+)", "scan the room"),
            (r"look\s+(?:at\s+)?(?:the\s+)?(?P<object>\w+)", "look at the object"),
            (r"detect\s+(?:the\s+)?(?P<object>\w+)", "detect the person"),
            (r"observe\s+(?:the\s+)?(?P<area>\w+)", "observe the area"),
        ]

        for pattern, utterance in test_cases:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"

    def test_query_patterns_match(self):
        """Test that query patterns match expected utterances."""
        test_cases = [
            (r"where\s+are\s+you", "where are you"),
            (r"what\s+is\s+your\s+status", "what is your status"),
            (r"battery\s+level", "battery level"),
            (r"what\s+is\s+your\s+position", "what is your position"),
            (r"report\s+status", "report status"),
        ]

        for pattern, utterance in test_cases:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"

    def test_safety_patterns_match(self):
        """Test that safety patterns match expected utterances."""
        test_cases = [
            (r"^stop$", "stop"),
            (r"emergency\s+stop", "emergency stop"),
            (r"^halt$", "halt"),
            (r"^freeze$", "freeze"),
            (r"^abort$", "abort"),
        ]

        for pattern, utterance in test_cases:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"

    def test_configure_patterns_match(self):
        """Test that configure patterns match expected utterances."""
        patterns = [
            (r"set\s+speed\s+to\s+(?P<speed>\w+)", "set speed to fast", "fast"),
            (r"set\s+mode\s+to\s+(?P<mode>\w+)", "set mode to auto", "auto"),
            (r"enable\s+(?P<feature>\w+)", "enable sensors", "sensors"),
            (r"disable\s+(?P<feature>\w+)", "disable lights", "lights"),
        ]

        for pattern, utterance, expected_value in patterns:
            compiled = re.compile(pattern, re.IGNORECASE)
            match = compiled.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"
            # Get the first group (speed, mode, or feature)
            group_name = list(compiled.groupindex.keys())[0]
            assert match.group(group_name) == expected_value

    def test_patterns_case_insensitive(self):
        """Test that patterns are case insensitive."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        variations = [
            "go to kitchen",
            "Go To Kitchen",
            "GO TO KITCHEN",
            "Go To kitchen",
        ]

        for utterance in variations:
            match = pattern.search(utterance)
            assert match is not None, f"Pattern failed to match: {utterance}"
            assert match.group("location").lower() == "kitchen"

    def test_patterns_no_match(self):
        """Test that patterns don't match unrelated utterances."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        non_matching = [
            "pick up the cup",
            "what do you see",
            "stop",
            "hello world",
        ]

        for utterance in non_matching:
            match = pattern.search(utterance)
            assert match is None, f"Pattern should not match: {utterance}"

    def test_entity_type_mapping(self):
        """Test entity type mapping logic."""
        mapping = {
            "location": "LOCATION",
            "object": "OBJECT",
            "area": "LOCATION",
            "speed": "SPEED",
            "mode": "MODE",
            "feature": "FEATURE",
        }

        test_cases = [
            ("location", "LOCATION"),
            ("object", "OBJECT"),
            ("area", "LOCATION"),
            ("speed", "SPEED"),
            ("mode", "MODE"),
            ("feature", "FEATURE"),
            ("unknown", "UNKNOWN"),  # Default case
        ]

        for group_name, expected_type in test_cases:
            result = mapping.get(group_name, group_name.upper())
            assert result == expected_type

    def test_intent_confidence_scoring(self):
        """Test intent confidence scoring logic."""
        # Direct pattern match should have high confidence
        direct_match_confidence = 0.95

        # Medium confidence for partial matches
        medium_confidence = 0.70

        # Low confidence for unknown
        unknown_confidence = 0.30

        assert direct_match_confidence > medium_confidence
        assert medium_confidence > unknown_confidence

    def test_performance_targets(self):
        """Test that performance targets are defined."""
        TARGET_LATENCY_MS = 10.0
        TARGET_CONFIDENCE = 0.95

        assert TARGET_LATENCY_MS == 10.0
        assert TARGET_CONFIDENCE == 0.95

    def test_latency_tracking(self):
        """Test latency tracking mechanism."""
        latency_history = []
        max_history_size = 1000

        # Simulate adding latencies
        for i in range(10):
            latency_history.append(float(i))
            if len(latency_history) > max_history_size:
                latency_history.pop(0)

        assert len(latency_history) == 10

        # Test history limit
        latency_history = []
        for i in range(max_history_size + 100):
            latency_history.append(float(i))
            if len(latency_history) > max_history_size:
                latency_history.pop(0)

        assert len(latency_history) == max_history_size

    def test_performance_stats_calculation(self):
        """Test performance statistics calculation."""
        latencies = [5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0]

        # Calculate statistics
        avg = sum(latencies) / len(latencies)
        sorted_latencies = sorted(latencies)
        p50 = sorted_latencies[len(sorted_latencies) // 2]
        p95_index = int(len(sorted_latencies) * 0.95)
        p95 = sorted_latencies[min(p95_index, len(sorted_latencies) - 1)]

        assert avg == 27.5
        # For even-length list, median is between two middle values
        # Index 5 is the 6th element (0-indexed), which is 30.0
        assert p50 == 30.0  # Median for even-length list
        # p95 for 10 items at index 9 (95th percentile) = 50.0
        assert p95 == 50.0  # 95th percentile should be the max for this dataset

    def test_empty_utterance_handling(self):
        """Test handling of empty utterances."""
        empty_utterances = ["", "   ", "\t", "\n"]

        for utterance in empty_utterances:
            stripped = utterance.lower().strip()
            assert stripped == ""

    def test_complex_utterance_parsing(self):
        """Test parsing of complex utterances."""
        # Complex utterances that might need LLM fallback
        complex_utterances = [
            "go to the kitchen but avoid the living room and don't bump into furniture",
            "pick up the red cup from the table near the window",
            "navigate to the charging station while avoiding obstacles",
        ]

        # These should not match simple patterns
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        for utterance in complex_utterances:
            match = pattern.search(utterance)
            # First pattern might match but not capture full intent
            if match:
                assert match.group("location") in ["kitchen", "charging"]


class TestIntentParserPatternEdgeCases:
    """Test edge cases for pattern matching."""

    def test_pattern_with_special_characters(self):
        """Test patterns with special characters in input."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        # Should handle alphanumeric locations
        match = pattern.search("go to room123")
        assert match is not None
        assert match.group("location") == "room123"

    def test_pattern_with_underscores(self):
        """Test patterns with underscores in location names."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        match = pattern.search("go to charging_station")
        assert match is not None
        assert match.group("location") == "charging_station"

    def test_multiple_entities_in_utterance(self):
        """Test extracting multiple entities from utterance."""
        # This tests the logic for extracting multiple entities
        utterance = "pick up the cup from the table"

        object_pattern = re.compile(r"pick\s+up\s+(?:the\s+)?(?P<object>\w+)", re.IGNORECASE)
        location_pattern = re.compile(r"from\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        object_match = object_pattern.search(utterance)
        location_match = location_pattern.search(utterance)

        assert object_match is not None
        assert object_match.group("object") == "cup"

        assert location_match is not None
        assert location_match.group("location") == "table"

    def test_partial_match_handling(self):
        """Test handling of partial matches."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        # Partial match - "go to" without location
        match = pattern.search("go to")
        assert match is None  # Should not match without location

    def test_overlapping_patterns(self):
        """Test handling of overlapping patterns."""
        navigate_pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)
        manipulate_pattern = re.compile(r"pick\s+up\s+(?:the\s+)?(?P<object>\w+)", re.IGNORECASE)

        utterance = "go to the kitchen"

        nav_match = navigate_pattern.search(utterance)
        man_match = manipulate_pattern.search(utterance)

        assert nav_match is not None
        assert man_match is None

    def test_whitespace_handling(self):
        """Test handling of various whitespace patterns."""
        pattern = re.compile(r"go\s+to\s+(?:the\s+)?(?P<location>\w+)", re.IGNORECASE)

        variations = [
            "go to kitchen",
            "go  to kitchen",  # Extra space
            "go\tto kitchen",  # Tab
            "go to  kitchen",  # Extra space before location
        ]

        for utterance in variations:
            match = pattern.search(utterance)
            assert match is not None, f"Pattern failed to match: {repr(utterance)}"


class TestIntentParserSourceTracking:
    """Test intent source tracking (RULE_BASED vs LLM_ASSISTED)."""

    def test_source_determination_high_confidence(self):
        """High confidence intents are marked as RULE_BASED."""
        confidence = 0.95

        if confidence >= 0.95 or confidence >= 0.70:
            source = "RULE_BASED"
        else:
            source = "LLM_ASSISTED"

        assert source == "RULE_BASED"

    def test_source_determination_medium_confidence(self):
        """Medium confidence intents are marked as RULE_BASED."""
        confidence = 0.75

        if confidence >= 0.95 or confidence >= 0.70:
            source = "RULE_BASED"
        else:
            source = "LLM_ASSISTED"

        assert source == "RULE_BASED"

    def test_source_determination_low_confidence(self):
        """Low confidence intents use LLM fallback."""
        confidence = 0.50

        if confidence >= 0.95 or confidence >= 0.70:
            source = "RULE_BASED"
        else:
            source = "LLM_ASSISTED"

        assert source == "LLM_ASSISTED"


class TestIntentParserLLMFallback:
    """Test LLM fallback logic."""

    def test_llm_fallback_triggered(self):
        """Test that LLM fallback is triggered for low confidence."""
        confidence = 0.30

        needs_llm_fallback = confidence < 0.70

        assert needs_llm_fallback is True

    def test_llm_fallback_not_triggered(self):
        """Test that LLM fallback is not triggered for high confidence."""
        confidence = 0.95

        needs_llm_fallback = confidence < 0.70

        assert needs_llm_fallback is False

    def test_llm_parser_availability_check(self):
        """Test LLM parser availability check logic."""
        # Simulate LLM parser availability
        llm_available = False  # Simulating unavailable LLM

        can_use_llm = llm_available

        assert can_use_llm is False

    def test_llm_fallback_result_structure(self):
        """Test structure of LLM fallback result."""
        # Simulate LLM result
        llm_result = {
            "intent_type": "NAVIGATE",
            "confidence": 0.85,
            "entities": [
                {"type": "LOCATION", "value": "kitchen"},
            ],
        }

        assert "intent_type" in llm_result
        assert "confidence" in llm_result
        assert "entities" in llm_result
        assert len(llm_result["entities"]) > 0
