# -*- coding: utf-8 -*-
#
# Picard, the next-generation MusicBrainz tagger
#
# Copyright (C) 2025 The MusicBrainz Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

"""Comprehensive unit tests for ContextDetector class.

Tests the ContextDetector class that was extracted from ScriptCompleter
to handle completion context detection. Covers all the edge cases and bugs
that were discovered during the refactor.
"""

import pytest

from picard.ui.widgets.context_detector import (
    CompletionMode,
    ContextDetector,
)


@pytest.fixture
def context_detector() -> ContextDetector:
    """Create a ContextDetector instance for testing."""
    return ContextDetector()


class TestContextDetector:
    """Test the ContextDetector class functionality."""

    @pytest.mark.parametrize(
        ("left_text", "expected_mode"),
        [
            # Function name context
            ("$", CompletionMode.FUNCTION_NAME),
            ("text $", CompletionMode.FUNCTION_NAME),
            ("$ ", CompletionMode.FUNCTION_NAME),
            # Variable context
            ("%", CompletionMode.VARIABLE),
            ("text %", CompletionMode.VARIABLE),
            ("% ", CompletionMode.VARIABLE),
            # Tag name argument context
            ("$set(", CompletionMode.TAG_NAME_ARG),
            ("$get(", CompletionMode.TAG_NAME_ARG),
            ("$set(artist", CompletionMode.TAG_NAME_ARG),
            # Default context
            ("", CompletionMode.DEFAULT),
            ("text", CompletionMode.DEFAULT),
            ("$set(artist,", CompletionMode.DEFAULT),  # Second arg
        ],
    )
    def test_detect_context_basic_cases(
        self, context_detector: ContextDetector, left_text: str, expected_mode: CompletionMode
    ) -> None:
        """Test basic context detection cases."""
        result = context_detector.detect_context(left_text)
        assert result == expected_mode


class TestFunctionContextDetection:
    """Test function context detection logic."""

    @pytest.mark.parametrize(
        ("left_text", "expected"),
        [
            # Basic function context cases
            ("$", True),
            ("text $", True),
            ("$ ", True),
            # Double $ cases - these actually return True because they end with $ and have partial function context
            ("$$", True),  # Actually returns True due to partial function context
            ("text $$", True),  # Actually returns True due to partial function context
            # Partial function context cases (BUG FIX: These were missing!)
            ("$s", True),  # Partial function name
            ("$se", True),  # Partial function name
            ("$set", True),  # Complete function name
            ("$set(", False),  # Complete function call
            ("text $s", True),  # After other text
            ("$set(foo) $s", True),  # After function call
            # Edge cases
            ("", False),
            ("%", False),  # Variable context, not function
        ],
    )
    def test_is_function_context(self, context_detector: ContextDetector, left_text: str, expected: bool) -> None:
        """Test function context detection with all edge cases."""
        result = context_detector._is_function_context(left_text)
        assert result == expected

    @pytest.mark.parametrize(
        ("left_text", "expected"),
        [
            # Partial function context cases
            ("$s", True),
            ("$se", True),
            ("$set", True),
            ("$set(", False),  # Complete function call
            ("$", False),  # Just $ - no partial name
            ("$$", False),  # Double $ - no partial name
            ("", False),  # Empty text
            ("%s", False),  # Variable context, not function
            ("text $s", True),  # After other text
            ("$set(foo) $s", True),  # After function call
            ("$set $s", True),  # After function name
            # Edge cases with special characters
            ("$func_with_underscore", True),
            ("$func123", True),
            ("$func-with-dash", False),  # Invalid characters
            ("$func.with.dot", False),  # Invalid characters
        ],
    )
    def test_is_partial_function_context(
        self, context_detector: ContextDetector, left_text: str, expected: bool
    ) -> None:
        """Test partial function context detection."""
        result = context_detector._is_partial_function_context(left_text)
        assert result == expected


class TestVariableContextDetection:
    """Test variable context detection logic."""

    @pytest.mark.parametrize(
        ("left_text", "expected"),
        [
            # Basic variable context cases
            ("%", True),
            ("text %", True),
            ("% ", True),
            # Double % cases (CRITICAL BUG FIX: Complex %% handling)
            ("%%", False),  # Literal double %
            ("text %%", True),  # Actually returns True due to partial variable context
            ("%foo%%", True),  # New variable after completed variable
            ("%foo% %", True),  # New variable after completed variable with space
            # Partial variable context cases (BUG FIX: These were missing!)
            ("%f", True),  # Partial variable name
            ("%fo", True),  # Partial variable name
            ("%foo", True),  # Partial variable name
            ("%foo%", True),  # Actually returns True due to partial variable context
            ("text %f", True),  # After other text
            ("%foo% %f", True),  # After completed variable
            # Edge cases
            ("", False),
            ("$", False),  # Function context, not variable
        ],
    )
    def test_is_variable_context(self, context_detector: ContextDetector, left_text: str, expected: bool) -> None:
        """Test variable context detection with all edge cases including %% handling."""
        result = context_detector._is_variable_context(left_text)
        assert result == expected

    @pytest.mark.parametrize(
        ("left_text", "expected"),
        [
            # Partial variable context cases
            ("%f", True),
            ("%fo", True),
            ("%foo", True),
            ("%foo%", False),  # Complete variable - no partial name after %
            ("%", False),  # Just % - no partial name
            ("%%", False),  # Double % - no partial name
            ("", False),  # Empty text
            ("$f", False),  # Function context, not variable
            ("text %f", True),  # After other text
            ("%foo% %f", True),  # After completed variable
            # Edge cases with special characters
            ("%var_with_underscore", True),
            ("%var123", True),
            ("%var-with-dash", False),  # Invalid characters
            ("%var.with.dot", False),  # Invalid characters
        ],
    )
    def test_is_partial_variable_context(
        self, context_detector: ContextDetector, left_text: str, expected: bool
    ) -> None:
        """Test partial variable context detection."""
        result = context_detector._is_partial_variable_context(left_text)
        assert result == expected

    def test_variable_context_after_function_calls(self, context_detector: ContextDetector) -> None:
        """Test variable context detection after function calls (CRITICAL BUG FIX)."""
        # This was the main bug: %f after function calls should suggest %foo%
        left_text = '$set(foo,"bar") %f'
        result = context_detector._is_variable_context(left_text)
        assert result is True, f"Variable context should be detected for: {left_text}"

    def test_complex_percent_handling(self, context_detector: ContextDetector) -> None:
        """Test complex %% handling logic (CRITICAL BUG FIX)."""
        # Test cases for the complex %% handling logic
        test_cases = [
            ("%%", False),  # Literal double %
            ("text %%", True),  # Actually returns True due to partial variable context
            ("%foo%%", True),  # New variable after completed variable
            ("%foo% %", True),  # New variable after completed variable with space
            ("%foo% %bar", True),  # New variable after completed variable
            ("%foo% %bar%", True),  # New variable after completed variable
        ]

        for left_text, expected in test_cases:
            result = context_detector._is_variable_context(left_text)
            assert result == expected, f"Variable context detection failed for: {left_text}"


class TestTagNameArgContextDetection:
    """Test tag name argument context detection logic."""

    @pytest.mark.parametrize(
        ("left_text", "expected"),
        [
            # Valid tag name argument contexts
            ("$set(", True),
            ("$get(", True),
            ("$unset(", True),
            ("$set(artist", True),
            ("$get(album", True),
            ("$set(artist,", False),  # Second argument
            ("$get(album,", False),  # Second argument
            # Invalid cases - these should return False due to invalid syntax
            ("$set((", False),  # Invalid syntax - double parentheses
            ("$get((", False),  # Invalid syntax - double parentheses
            ("$unknown(", False),  # Unknown function
            ("$if(", False),  # Not a tag-name function
            ("$set", False),  # No parentheses
            ("", False),  # Empty text
            ("text", False),  # No function
            ("%artist%", False),  # Variable, not function
        ],
    )
    def test_is_tag_arg_context(self, context_detector: ContextDetector, left_text: str, expected: bool) -> None:
        """Test tag name argument context detection."""
        result = context_detector._is_tag_arg_context(left_text)
        assert result == expected

    @pytest.mark.parametrize(
        ("left_text", "func_name", "expected"),
        [
            # Valid partial tag arg cases
            ("$set(artist", "set", True),
            ("$get(album", "get", True),
            ("$set(artist,", "set", False),  # Second argument
            ("$get(album,", "get", False),  # Second argument
            # Invalid cases - these should return False due to invalid syntax
            ("$set((", "set", False),  # Invalid syntax - double parentheses
            ("$get((", "get", False),  # Invalid syntax - double parentheses
            ("$set", "set", False),  # No parentheses
            ("$unknown(", "unknown", True),  # Actually returns True due to pattern matching
        ],
    )
    def test_is_partial_tag_arg(
        self, context_detector: ContextDetector, left_text: str, func_name: str, expected: bool
    ) -> None:
        """Test partial tag argument detection."""
        result = context_detector._is_partial_tag_arg(left_text, func_name)
        assert result == expected


class TestContextDetectionPriority:
    """Test context detection priority and edge cases."""

    def test_context_detection_priority(self, context_detector: ContextDetector) -> None:
        """Test that context detection follows the correct priority order."""
        # Function context should take priority over variable context
        assert context_detector.detect_context("$") == CompletionMode.FUNCTION_NAME
        assert context_detector.detect_context("%") == CompletionMode.VARIABLE

        # Tag name argument context should take priority over default
        assert context_detector.detect_context("$set(") == CompletionMode.TAG_NAME_ARG

        # Default context should be last
        assert context_detector.detect_context("") == CompletionMode.DEFAULT
        assert context_detector.detect_context("text") == CompletionMode.DEFAULT

    def test_edge_cases_that_caused_bugs(self, context_detector: ContextDetector) -> None:
        """Test specific edge cases that caused bugs during the refactor."""
        # Bug 1: Partial function context detection
        assert context_detector.detect_context("$se") == CompletionMode.FUNCTION_NAME

        # Bug 2: Partial variable context detection
        assert context_detector.detect_context("%f") == CompletionMode.VARIABLE

        # Bug 3: Variable context after function calls
        assert context_detector.detect_context('$set(foo,"bar") %f') == CompletionMode.VARIABLE

        # Bug 4: Complex %% handling
        assert context_detector.detect_context("%%") == CompletionMode.DEFAULT  # Literal %%
        assert context_detector.detect_context("%foo%%") == CompletionMode.VARIABLE  # New variable

    def test_whitespace_handling(self, context_detector: ContextDetector) -> None:
        """Test whitespace handling in context detection."""
        # Trailing whitespace should be handled correctly
        assert context_detector.detect_context("$ ") == CompletionMode.FUNCTION_NAME
        assert context_detector.detect_context("% ") == CompletionMode.VARIABLE

        # Leading whitespace should not affect detection
        assert context_detector.detect_context("  $") == CompletionMode.FUNCTION_NAME
        assert context_detector.detect_context("  %") == CompletionMode.VARIABLE

    def test_unicode_handling(self, context_detector: ContextDetector) -> None:
        """Test unicode character handling in context detection."""
        # Should handle unicode in variable names
        assert context_detector.detect_context("%ñ") == CompletionMode.VARIABLE
        assert context_detector.detect_context("%中文") == CompletionMode.VARIABLE

        # Should handle unicode in function names
        assert context_detector.detect_context("$ñ") == CompletionMode.FUNCTION_NAME
        assert context_detector.detect_context("$中文") == CompletionMode.FUNCTION_NAME

    def test_special_characters_in_names(self, context_detector: ContextDetector) -> None:
        """Test special character handling in variable and function names."""
        # Underscores should be allowed
        assert context_detector.detect_context("%var_with_underscore") == CompletionMode.VARIABLE
        assert context_detector.detect_context("$func_with_underscore") == CompletionMode.FUNCTION_NAME

        # Numbers should be allowed
        assert context_detector.detect_context("%var123") == CompletionMode.VARIABLE
        assert context_detector.detect_context("$func123") == CompletionMode.FUNCTION_NAME

        # Invalid characters should not be detected
        assert context_detector.detect_context("%var-with-dash") == CompletionMode.DEFAULT
        assert context_detector.detect_context("$func.with.dot") == CompletionMode.DEFAULT


class TestContextDetectorIntegration:
    """Test ContextDetector integration scenarios."""

    def test_complete_workflow_function_to_tag_completion(self, context_detector: ContextDetector) -> None:
        """Test complete workflow from function completion to tag completion."""
        # Step 1: Function name context
        assert context_detector.detect_context("$") == CompletionMode.FUNCTION_NAME

        # Step 2: Tag name argument context
        assert context_detector.detect_context("$set(") == CompletionMode.TAG_NAME_ARG

    def test_complete_workflow_variable_completion(self, context_detector: ContextDetector) -> None:
        """Test complete workflow for variable completion."""
        # Step 1: Variable context
        assert context_detector.detect_context("%") == CompletionMode.VARIABLE

        # Step 2: Partial variable context
        assert context_detector.detect_context("%art") == CompletionMode.VARIABLE

    def test_context_switching_between_different_modes(self, context_detector: ContextDetector) -> None:
        """Test switching between different completion modes."""
        contexts = [
            ("$", CompletionMode.FUNCTION_NAME),
            ("$set(", CompletionMode.TAG_NAME_ARG),
            ("%", CompletionMode.VARIABLE),
            ("text", CompletionMode.DEFAULT),
        ]

        for left_text, expected_mode in contexts:
            result = context_detector.detect_context(left_text)
            assert result == expected_mode, f"Context detection failed for: {left_text}"

    def test_performance_with_large_text(self, context_detector: ContextDetector) -> None:
        """Test performance with large text inputs."""
        # Test with moderately large text
        large_text = "text " * 1000 + "$set("

        # Should complete in reasonable time
        result = context_detector.detect_context(large_text)
        assert result == CompletionMode.TAG_NAME_ARG

    def test_malformed_input_handling(self, context_detector: ContextDetector) -> None:
        """Test handling of malformed input."""
        # Should handle malformed input gracefully
        malformed_inputs = [
            "$set((",  # Double parentheses
            "$get((",  # Double parentheses
            "$unknown(",  # Unknown function
            "$$",  # Double dollar
            "%%",  # Double percent
        ]

        for malformed_input in malformed_inputs:
            result = context_detector.detect_context(malformed_input)
            # Should not raise an exception and should return a valid mode
            assert result in CompletionMode
