#!/usr/bin/env python3
"""
Tests for error_patterns.py module

Run with:
    pytest chengyue_script/dvlm/evaluation/tests/test_error_patterns.py -v
    python -m pytest chengyue_script/dvlm/evaluation/tests/test_error_patterns.py -v
"""

import sys
from pathlib import Path

# Add parent directory to path
test_dir = Path(__file__).parent
eval_dir = test_dir.parent
if str(eval_dir) not in sys.path:
    sys.path.insert(0, str(eval_dir))

from error_patterns import (
    # Utilities
    normalize_text_basic,
    strip_punct,
    extract_numbers,
    extract_numbers_normalized,
    tokenize_words,
    compute_word_overlap,
    compute_char_overlap,
    levenshtein_distance,
    levenshtein_ratio,
    is_yes_no_answer,
    detect_yes_no_flip,
    detect_repetition,
    # Classification
    classify_error_v2,
    ErrorPatternResult,
    # Counting
    count_patterns,
    pattern_summary_table,
    PATTERN_DESCRIPTIONS,
)


class TestNormalization:
    """Test text normalization utilities."""
    
    def test_normalize_text_basic(self):
        assert normalize_text_basic("  Hello   World  ") == "hello world"
        assert normalize_text_basic("UPPER") == "upper"
        assert normalize_text_basic(None) == ""
        assert normalize_text_basic("") == ""
    
    def test_strip_punct(self):
        assert strip_punct("hello, world!") == "hello world"
        assert strip_punct("test.") == "test"
        assert strip_punct("no-hyphen") == "nohyphen"
    
    def test_extract_numbers(self):
        assert extract_numbers("42") == ["42"]
        assert extract_numbers("3.14") == ["3.14"]
        assert extract_numbers("-10") == ["-10"]
        assert extract_numbers("100%") == ["100%"]
        assert extract_numbers("1,000") == ["1,000"]
        assert extract_numbers("no numbers") == []
        assert set(extract_numbers("12 and 34")) == {"12", "34"}
    
    def test_extract_numbers_normalized(self):
        assert extract_numbers_normalized("42") == [42.0]
        assert extract_numbers_normalized("1,000") == [1000.0]
        assert extract_numbers_normalized("50%") == [50.0]
    
    def test_tokenize_words(self):
        assert tokenize_words("Hello, World!") == ["hello", "world"]
        assert tokenize_words("a b c") == ["a", "b", "c"]
        assert tokenize_words("") == []


class TestOverlapMetrics:
    """Test overlap and distance metrics."""
    
    def test_word_overlap(self):
        ratio, overlap, union = compute_word_overlap("hello world", "hello there")
        assert overlap == 1  # "hello"
        assert union == 3  # "hello", "world", "there"
        assert 0.3 < ratio < 0.4
        
        ratio, _, _ = compute_word_overlap("same words", "same words")
        assert ratio == 1.0
    
    def test_char_overlap(self):
        ratio, matches = compute_char_overlap("hello", "hallo")
        assert matches == 4  # h, l, l, o match
        assert 0.7 < ratio < 0.9
        
        ratio, _ = compute_char_overlap("abc", "abc")
        assert ratio == 1.0
    
    def test_levenshtein_distance(self):
        assert levenshtein_distance("kitten", "sitting") == 3
        assert levenshtein_distance("abc", "abc") == 0
        assert levenshtein_distance("", "abc") == 3
        assert levenshtein_distance("abc", "") == 3
    
    def test_levenshtein_ratio(self):
        assert levenshtein_ratio("abc", "abc") == 0.0
        assert levenshtein_ratio("abc", "xyz") == 1.0


class TestYesNoDetection:
    """Test yes/no answer detection."""
    
    def test_is_yes_no_answer(self):
        is_yn, pol = is_yes_no_answer("yes")
        assert is_yn is True
        assert pol == "yes"
        
        is_yn, pol = is_yes_no_answer("No")
        assert is_yn is True
        assert pol == "no"
        
        is_yn, pol = is_yes_no_answer("true")
        assert is_yn is True
        assert pol == "yes"
        
        is_yn, pol = is_yes_no_answer("false")
        assert is_yn is True
        assert pol == "no"
        
        is_yn, pol = is_yes_no_answer("maybe")
        assert is_yn is False
        assert pol is None
    
    def test_detect_yes_no_flip(self):
        is_flip, flip_type = detect_yes_no_flip("no", "yes")
        assert is_flip is True
        assert flip_type == "yes→no"
        
        is_flip, flip_type = detect_yes_no_flip("yes", "no")
        assert is_flip is True
        assert flip_type == "no→yes"
        
        is_flip, _ = detect_yes_no_flip("yes", "yes")
        assert is_flip is False
        
        is_flip, _ = detect_yes_no_flip("apple", "yes")
        assert is_flip is False


class TestRepetitionDetection:
    """Test repetition pattern detection."""
    
    def test_detect_repetition(self):
        has_rep, pattern, count = detect_repetition("hello hello hello hello")
        assert has_rep is True
        assert pattern == "hello"
        assert count >= 4
        
        has_rep, _, _ = detect_repetition("hello world")
        assert has_rep is False
        
        has_rep, _, _ = detect_repetition("a b c d e")
        assert has_rep is False


class TestPatternClassification:
    """Test the main classify_error_v2 function."""
    
    def test_empty_or_null(self):
        result = classify_error_v2("", "hello world")
        assert result.pattern == "empty_or_null"
        assert result.confidence == 1.0
    
    def test_severe_truncation(self):
        result = classify_error_v2("ab", "hello world this is long")
        assert result.pattern == "severe_truncation"
    
    def test_prefix_truncation(self):
        result = classify_error_v2("hel", "hello world")
        assert result.pattern == "prefix_truncation"
    
    def test_noise_coverage(self):
        result = classify_error_v2("The answer is hello and more stuff here blah blah", "hello")
        assert result.pattern == "noise_coverage"
    
    def test_yes_no_flip(self):
        result = classify_error_v2("no", "yes")
        assert result.pattern == "yes_no_flip"
        
        result = classify_error_v2("yes", "no")
        assert result.pattern == "yes_no_flip"
    
    def test_numeric_mismatch(self):
        result = classify_error_v2("42", "43")
        assert result.pattern == "numeric_mismatch"
    
    def test_case_punct_only(self):
        result = classify_error_v2("Hello, World!", "hello world")
        assert result.pattern == "case_punct_only"
    
    def test_repetition_loop(self):
        result = classify_error_v2("test test test test test", "hello")
        assert result.pattern == "repetition_loop"
    
    def test_completely_wrong(self):
        result = classify_error_v2("apple banana cherry", "xyz uvw rst")
        assert result.pattern == "completely_wrong"
    
    def test_char_level_typo(self):
        result = classify_error_v2("hellp", "hello")
        assert result.pattern == "char_level_typo"
    
    def test_list_ground_truth(self):
        """Test that list ground truth is handled correctly."""
        result = classify_error_v2("wrong", ["correct", "also correct"])
        assert result is not None
        assert result.pattern == "completely_wrong"


class TestCounting:
    """Test pattern counting utilities."""
    
    def test_count_patterns(self):
        results = [
            ErrorPatternResult(pattern="empty_or_null", confidence=1.0),
            ErrorPatternResult(pattern="empty_or_null", confidence=1.0),
            ErrorPatternResult(pattern="yes_no_flip", confidence=0.9),
        ]
        counts = count_patterns(results)
        assert counts["empty_or_null"] == 2
        assert counts["yes_no_flip"] == 1
    
    def test_pattern_summary_table(self):
        counts = {"a": 10, "b": 5, "c": 5}
        rows = pattern_summary_table(counts, total=20)
        
        assert len(rows) == 3
        assert rows[0]["pattern"] == "a"
        assert rows[0]["count"] == 10
        assert rows[0]["percentage"] == 50.0


class TestPatternDescriptions:
    """Test that all patterns have descriptions."""
    
    def test_all_patterns_documented(self):
        """All patterns returned by classify should have descriptions."""
        known_patterns = [
            "empty_or_null", "severe_truncation", "prefix_truncation",
            "too_short_general", "noise_coverage", "too_long_general",
            "repetition_loop", "yes_no_flip", "yes_no_with_explanation",
            "numeric_mismatch", "unit_mismatch", "format_numeric",
            "case_punct_only", "whitespace_only", "char_level_typo",
            "substring_partial", "entity_confusion", "completely_wrong",
            "other",
        ]
        
        for pattern in known_patterns:
            assert pattern in PATTERN_DESCRIPTIONS, f"Missing description for: {pattern}"
            assert len(PATTERN_DESCRIPTIONS[pattern]) > 0


if __name__ == "__main__":
    import pytest
    pytest.main([__file__, "-v"])

