#!/usr/bin/env python3
"""
Error Pattern Classification Module v2

Provides a comprehensive taxonomy for classifying prediction errors in VLM evaluation.
Designed to be reusable across different analysis scripts.

Pattern Categories:
- Length/truncation patterns
- Yes/No polarity patterns
- Numeric patterns
- OCR/string-level patterns
- Semantic patterns

Usage:
    from error_patterns import classify_error_v2, ErrorPatternResult, count_patterns
    
    result = classify_error_v2(prediction, ground_truth, question, benchmark)
    print(result.pattern, result.subpattern, result.confidence)
"""

import re
from typing import Dict, List, Optional, Tuple, Any
from dataclasses import dataclass, field
from collections import Counter


# ==============================================================================
# Data Classes
# ==============================================================================

@dataclass
class ErrorPatternResult:
    """Result of error pattern classification."""
    pattern: str  # Main pattern category
    subpattern: Optional[str] = None  # Optional sub-classification
    confidence: float = 0.0  # 0-1 confidence score
    diagnostics: Dict[str, Any] = field(default_factory=dict)  # Additional info
    
    def to_dict(self) -> Dict[str, Any]:
        return {
            "pattern": self.pattern,
            "subpattern": self.subpattern,
            "confidence": self.confidence,
            "diagnostics": self.diagnostics,
        }


# ==============================================================================
# Text Normalization Utilities
# ==============================================================================

def normalize_text_basic(s: str) -> str:
    """Lowercase, strip, collapse whitespace."""
    if s is None:
        return ""
    s = str(s).strip().lower()
    s = re.sub(r'\s+', ' ', s)
    return s


def strip_punct(s: str) -> str:
    """Remove punctuation, keep alphanumeric and spaces."""
    return re.sub(r'[^\w\s]', '', s)


def extract_numbers(s: str) -> List[str]:
    """Extract all numeric values (including decimals, negatives, percentages)."""
    # Match numbers like: 123, -45, 3.14, 10%, 1,000
    return re.findall(r'-?\d[\d,]*\.?\d*%?', s)


def extract_numbers_normalized(s: str) -> List[float]:
    """Extract numbers and convert to float (remove commas, %)."""
    nums = extract_numbers(s)
    result = []
    for n in nums:
        try:
            # Remove commas and %
            clean = n.replace(',', '').replace('%', '')
            result.append(float(clean))
        except ValueError:
            continue
    return result


def tokenize_words(s: str) -> List[str]:
    """Split into word tokens (lowercase, no punct)."""
    clean = strip_punct(normalize_text_basic(s))
    return clean.split()


def compute_word_overlap(s1: str, s2: str) -> Tuple[float, int, int]:
    """
    Compute word-level overlap ratio.
    Returns (overlap_ratio, overlap_count, union_count)
    """
    words1 = set(tokenize_words(s1))
    words2 = set(tokenize_words(s2))
    if not words1 or not words2:
        return 0.0, 0, 0
    overlap = len(words1 & words2)
    union = len(words1 | words2)
    return overlap / union if union > 0 else 0.0, overlap, union


def compute_char_overlap(s1: str, s2: str) -> Tuple[float, int]:
    """
    Compute character-level overlap for aligned strings.
    Returns (overlap_ratio, matching_chars)
    """
    s1 = normalize_text_basic(s1)
    s2 = normalize_text_basic(s2)
    if not s1 or not s2:
        return 0.0, 0
    min_len = min(len(s1), len(s2))
    matches = sum(1 for a, b in zip(s1, s2) if a == b)
    return matches / min_len, matches


def levenshtein_distance(s1: str, s2: str) -> int:
    """Compute Levenshtein edit distance."""
    if len(s1) < len(s2):
        return levenshtein_distance(s2, s1)
    if len(s2) == 0:
        return len(s1)
    
    prev_row = list(range(len(s2) + 1))
    for i, c1 in enumerate(s1):
        curr_row = [i + 1]
        for j, c2 in enumerate(s2):
            insertions = prev_row[j + 1] + 1
            deletions = curr_row[j] + 1
            substitutions = prev_row[j] + (c1 != c2)
            curr_row.append(min(insertions, deletions, substitutions))
        prev_row = curr_row
    return prev_row[-1]


def levenshtein_ratio(s1: str, s2: str) -> float:
    """Compute normalized edit distance ratio (0 = identical, 1 = completely different)."""
    s1 = normalize_text_basic(s1)
    s2 = normalize_text_basic(s2)
    if not s1 and not s2:
        return 0.0
    max_len = max(len(s1), len(s2))
    if max_len == 0:
        return 0.0
    return levenshtein_distance(s1, s2) / max_len


# ==============================================================================
# Yes/No Detection
# ==============================================================================

YES_WORDS = {'yes', 'true', 'correct', 'right', 'yeah', 'yep', 'affirmative'}
NO_WORDS = {'no', 'false', 'incorrect', 'wrong', 'nope', 'negative'}


def is_yes_no_answer(s: str) -> Tuple[bool, Optional[str]]:
    """
    Check if string is a yes/no answer.
    Returns (is_yes_no, polarity) where polarity is 'yes', 'no', or None.
    """
    s_norm = normalize_text_basic(s)
    words = s_norm.split()
    
    # Check first word or entire string
    first_word = words[0] if words else ""
    
    if first_word in YES_WORDS or s_norm in YES_WORDS:
        return True, 'yes'
    if first_word in NO_WORDS or s_norm in NO_WORDS:
        return True, 'no'
    
    # Check if yes/no appears anywhere
    for w in words:
        if w in YES_WORDS:
            return True, 'yes'
        if w in NO_WORDS:
            return True, 'no'
    
    return False, None


def detect_yes_no_flip(pred: str, gt: str) -> Tuple[bool, Optional[str]]:
    """
    Detect if prediction flipped a yes/no answer.
    Returns (is_flip, flip_type) where flip_type is 'yes→no' or 'no→yes'.
    """
    pred_yn, pred_pol = is_yes_no_answer(pred)
    gt_yn, gt_pol = is_yes_no_answer(gt)
    
    if not gt_yn:
        return False, None
    
    if pred_pol and gt_pol and pred_pol != gt_pol:
        return True, f"{gt_pol}→{pred_pol}"
    
    return False, None


# ==============================================================================
# Repetition Detection
# ==============================================================================

def detect_repetition(s: str, min_repeat: int = 3) -> Tuple[bool, Optional[str], int]:
    """
    Detect if string contains repetitive patterns.
    Returns (has_repetition, repeated_pattern, repeat_count).
    """
    s_norm = normalize_text_basic(s)
    words = s_norm.split()
    
    if len(words) < min_repeat:
        return False, None, 0
    
    # Check for repeated words
    word_counts = Counter(words)
    for word, count in word_counts.most_common(3):
        if count >= min_repeat and len(word) > 1:
            return True, word, count
    
    # Check for repeated phrases (2-3 word ngrams)
    for n in [2, 3]:
        if len(words) < n * min_repeat:
            continue
        ngrams = [' '.join(words[i:i+n]) for i in range(len(words) - n + 1)]
        ngram_counts = Counter(ngrams)
        for ngram, count in ngram_counts.most_common(1):
            if count >= min_repeat:
                return True, ngram, count
    
    return False, None, 0


# ==============================================================================
# Pattern Classification Functions
# ==============================================================================

def check_empty_or_null(pred: str) -> Optional[ErrorPatternResult]:
    """Check if prediction is empty/null."""
    pred_norm = normalize_text_basic(pred)
    if not pred_norm or pred_norm in ['nan', 'none', 'null', '']:
        return ErrorPatternResult(
            pattern="empty_or_null",
            confidence=1.0,
            diagnostics={"pred_len": len(pred_norm)}
        )
    return None


def check_severe_truncation(pred: str, gt: str, threshold: int = 3) -> Optional[ErrorPatternResult]:
    """Check for severe truncation (pred <= threshold chars, gt much longer)."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    # Skip if both contain numbers (likely numeric comparison, not truncation)
    if extract_numbers(pred) and extract_numbers(gt):
        return None
    
    if len(pred_norm) <= threshold and len(gt_norm) > threshold * 2:
        return ErrorPatternResult(
            pattern="severe_truncation",
            confidence=0.95,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "ratio": len(pred_norm) / len(gt_norm) if len(gt_norm) > 0 else 0
            }
        )
    return None


def check_prefix_truncation(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if pred is a prefix of gt (truncated mid-answer)."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(pred_norm) < 2 or len(gt_norm) < 4:
        return None
    
    # pred is prefix of gt
    if gt_norm.startswith(pred_norm) and len(pred_norm) < len(gt_norm) * 0.8:
        return ErrorPatternResult(
            pattern="prefix_truncation",
            confidence=0.9,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "truncation_point": len(pred_norm)
            }
        )
    return None


def check_too_short(pred: str, gt: str, ratio_threshold: float = 0.5) -> Optional[ErrorPatternResult]:
    """Check if pred is too short compared to gt."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(gt_norm) == 0:
        return None
    
    ratio = len(pred_norm) / len(gt_norm)
    if ratio < ratio_threshold and len(pred_norm) < len(gt_norm) - 5:
        return ErrorPatternResult(
            pattern="too_short_general",
            confidence=0.7,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "ratio": ratio
            }
        )
    return None


def check_noise_coverage(pred: str, gt: str, ratio_threshold: float = 2.0) -> Optional[ErrorPatternResult]:
    """Check if pred contains gt but is much longer (noise added)."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(gt_norm) < 2:
        return None
    
    if gt_norm in pred_norm and len(pred_norm) > len(gt_norm) * ratio_threshold:
        return ErrorPatternResult(
            pattern="noise_coverage",
            confidence=0.9,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "noise_ratio": len(pred_norm) / len(gt_norm),
                "gt_position": pred_norm.find(gt_norm)
            }
        )
    return None


def check_too_long(pred: str, gt: str, ratio_threshold: float = 2.0) -> Optional[ErrorPatternResult]:
    """Check if pred is too long and doesn't contain gt."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(gt_norm) == 0:
        return None
    
    if len(pred_norm) > len(gt_norm) * ratio_threshold and gt_norm not in pred_norm:
        return ErrorPatternResult(
            pattern="too_long_general",
            confidence=0.7,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "ratio": len(pred_norm) / len(gt_norm)
            }
        )
    return None


def check_repetition_loop(pred: str) -> Optional[ErrorPatternResult]:
    """Check for repetitive patterns in prediction."""
    has_rep, pattern, count = detect_repetition(pred)
    if has_rep:
        return ErrorPatternResult(
            pattern="repetition_loop",
            confidence=0.85,
            diagnostics={
                "repeated_pattern": pattern,
                "repeat_count": count
            }
        )
    return None


def check_yes_no_flip(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check for yes/no polarity flip."""
    is_flip, flip_type = detect_yes_no_flip(pred, gt)
    if is_flip:
        return ErrorPatternResult(
            pattern="yes_no_flip",
            subpattern=flip_type,
            confidence=0.95,
            diagnostics={"flip_type": flip_type}
        )
    return None


def check_yes_no_with_explanation(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if pred has correct yes/no but with extra explanation."""
    gt_yn, gt_pol = is_yes_no_answer(gt)
    if not gt_yn:
        return None
    
    pred_norm = normalize_text_basic(pred)
    words = pred_norm.split()
    
    if len(words) > 1:
        first_yn, first_pol = is_yes_no_answer(words[0])
        if first_yn and first_pol == gt_pol:
            return ErrorPatternResult(
                pattern="yes_no_with_explanation",
                confidence=0.8,
                diagnostics={
                    "polarity": first_pol,
                    "extra_words": len(words) - 1
                }
            )
    return None


def check_numeric_mismatch(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if both contain numbers but they differ."""
    pred_nums = extract_numbers_normalized(pred)
    gt_nums = extract_numbers_normalized(gt)
    
    if not pred_nums or not gt_nums:
        return None
    
    # Check if numbers are different
    if set(pred_nums) != set(gt_nums):
        return ErrorPatternResult(
            pattern="numeric_mismatch",
            confidence=0.85,
            diagnostics={
                "pred_numbers": pred_nums,
                "gt_numbers": gt_nums
            }
        )
    return None


def check_unit_mismatch(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if same number but different units."""
    pred_nums = extract_numbers_normalized(pred)
    gt_nums = extract_numbers_normalized(gt)
    
    if not pred_nums or not gt_nums:
        return None
    
    # Check for overlapping numbers
    if set(pred_nums) & set(gt_nums):
        # Numbers overlap, check for unit differences
        pred_norm = normalize_text_basic(pred)
        gt_norm = normalize_text_basic(gt)
        
        units = ['%', 'percent', 'cm', 'mm', 'm', 'km', 'kg', 'g', 'lb', 'oz', 
                 'hour', 'hours', 'min', 'minutes', 'sec', 'seconds', 'year', 'years',
                 'day', 'days', 'month', 'months', 'dollar', 'dollars', '$', '€', '£']
        
        pred_units = [u for u in units if u in pred_norm]
        gt_units = [u for u in units if u in gt_norm]
        
        if pred_units != gt_units and (pred_units or gt_units):
            return ErrorPatternResult(
                pattern="unit_mismatch",
                confidence=0.75,
                diagnostics={
                    "pred_units": pred_units,
                    "gt_units": gt_units
                }
            )
    return None


def check_format_numeric(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check for numeric formatting differences (commas, decimals)."""
    pred_nums = extract_numbers(pred)
    gt_nums = extract_numbers(gt)
    
    if not pred_nums or not gt_nums:
        return None
    
    # Normalize and compare
    pred_normalized = extract_numbers_normalized(pred)
    gt_normalized = extract_numbers_normalized(gt)
    
    if pred_normalized == gt_normalized and pred_nums != gt_nums:
        return ErrorPatternResult(
            pattern="format_numeric",
            confidence=0.9,
            diagnostics={
                "pred_format": pred_nums,
                "gt_format": gt_nums,
                "normalized_value": pred_normalized
            }
        )
    return None


def check_case_punct_only(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if pred matches gt after removing case and punctuation."""
    pred_clean = strip_punct(normalize_text_basic(pred))
    gt_clean = strip_punct(normalize_text_basic(gt))
    
    if pred_clean == gt_clean and pred.strip() != gt.strip():
        return ErrorPatternResult(
            pattern="case_punct_only",
            confidence=0.95,
            diagnostics={
                "original_pred": pred.strip()[:50],
                "original_gt": gt.strip()[:50]
            }
        )
    return None


def check_whitespace_only(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if difference is only whitespace (not case)."""
    # First check if they differ in case - if so, it's case_punct_only not whitespace
    pred_lower = pred.strip().lower()
    gt_lower = gt.strip().lower()
    
    # If lowercased versions are identical but originals differ only in case
    if pred_lower == gt_lower and pred.strip() != gt.strip():
        return None  # Let case_punct_only handle this
    
    pred_collapsed = re.sub(r'\s+', '', pred_lower)
    gt_collapsed = re.sub(r'\s+', '', gt_lower)
    
    if pred_collapsed == gt_collapsed and pred_lower != gt_lower:
        return ErrorPatternResult(
            pattern="whitespace_only",
            confidence=0.95,
            diagnostics={}
        )
    return None


def check_char_level_typo(pred: str, gt: str, threshold: float = 0.7) -> Optional[ErrorPatternResult]:
    """Check for character-level typo (high overlap but not exact)."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(pred_norm) < 3 or len(gt_norm) < 3:
        return None
    
    # Length should be similar
    len_ratio = len(pred_norm) / len(gt_norm) if len(gt_norm) > 0 else 0
    if not (0.7 <= len_ratio <= 1.3):
        return None
    
    char_overlap, matches = compute_char_overlap(pred_norm, gt_norm)
    
    if threshold <= char_overlap < 1.0:
        return ErrorPatternResult(
            pattern="char_level_typo",
            confidence=char_overlap,
            diagnostics={
                "char_overlap": char_overlap,
                "matching_chars": matches,
                "edit_distance": levenshtein_distance(pred_norm, gt_norm)
            }
        )
    return None


def check_substring_partial(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if pred is substring of gt or vice versa (but incomplete)."""
    pred_norm = normalize_text_basic(pred)
    gt_norm = normalize_text_basic(gt)
    
    if len(pred_norm) < 2 or len(gt_norm) < 2:
        return None
    
    if pred_norm in gt_norm and len(pred_norm) < len(gt_norm):
        return ErrorPatternResult(
            pattern="substring_partial",
            subpattern="pred_in_gt",
            confidence=0.75,
            diagnostics={
                "pred_len": len(pred_norm),
                "gt_len": len(gt_norm),
                "coverage": len(pred_norm) / len(gt_norm)
            }
        )
    
    if gt_norm in pred_norm and len(gt_norm) < len(pred_norm):
        # This is more like noise_coverage, skip
        return None
    
    return None


def check_entity_confusion(pred: str, gt: str) -> Optional[ErrorPatternResult]:
    """Check if pred shares some tokens with gt but key entity differs."""
    pred_words = set(tokenize_words(pred))
    gt_words = set(tokenize_words(gt))
    
    if not pred_words or not gt_words:
        return None
    
    overlap = pred_words & gt_words
    pred_only = pred_words - gt_words
    gt_only = gt_words - pred_words
    
    # Some overlap but also significant differences
    if len(overlap) >= 1 and len(pred_only) >= 1 and len(gt_only) >= 1:
        overlap_ratio = len(overlap) / len(gt_words)
        if 0.2 <= overlap_ratio <= 0.7:
            return ErrorPatternResult(
                pattern="entity_confusion",
                confidence=0.7,
                diagnostics={
                    "shared_tokens": list(overlap)[:5],
                    "pred_only": list(pred_only)[:5],
                    "gt_only": list(gt_only)[:5],
                    "overlap_ratio": overlap_ratio
                }
            )
    return None


def check_completely_wrong(pred: str, gt: str, threshold: float = 0.7) -> Optional[ErrorPatternResult]:
    """Check if pred is completely different from gt."""
    edit_ratio = levenshtein_ratio(pred, gt)
    word_overlap, _, _ = compute_word_overlap(pred, gt)
    
    if edit_ratio > threshold and word_overlap < 0.2:
        return ErrorPatternResult(
            pattern="completely_wrong",
            confidence=0.8,
            diagnostics={
                "edit_ratio": edit_ratio,
                "word_overlap": word_overlap
            }
        )
    return None


# ==============================================================================
# Main Classification Function
# ==============================================================================

def classify_error_v2(
    pred: str,
    gt: str,
    question: Optional[str] = None,
    benchmark: Optional[str] = None,
) -> ErrorPatternResult:
    """
    Classify a prediction error using the v2 taxonomy.
    
    Args:
        pred: Model prediction
        gt: Ground truth (can be string; if list, use first element)
        question: Optional question text (for context)
        benchmark: Optional benchmark name (for task-specific patterns)
    
    Returns:
        ErrorPatternResult with pattern, subpattern, confidence, and diagnostics
    """
    # Handle list gt
    if isinstance(gt, list):
        gt = gt[0] if gt else ""
    
    pred = str(pred) if pred is not None else ""
    gt = str(gt) if gt is not None else ""
    
    # Check patterns in priority order (most specific first)
    checks = [
        # Empty/null
        lambda: check_empty_or_null(pred),
        
        # Length patterns
        lambda: check_severe_truncation(pred, gt),
        lambda: check_prefix_truncation(pred, gt),
        lambda: check_noise_coverage(pred, gt),
        lambda: check_repetition_loop(pred),
        
        # Yes/No patterns
        lambda: check_yes_no_flip(pred, gt),
        lambda: check_yes_no_with_explanation(pred, gt),
        
        # Numeric patterns
        lambda: check_format_numeric(pred, gt),  # Check format first (exact match possible)
        lambda: check_unit_mismatch(pred, gt),
        lambda: check_numeric_mismatch(pred, gt),
        
        # String-level patterns (near-match)
        lambda: check_whitespace_only(pred, gt),
        lambda: check_case_punct_only(pred, gt),
        lambda: check_char_level_typo(pred, gt),
        lambda: check_substring_partial(pred, gt),
        
        # Length patterns (general)
        lambda: check_too_short(pred, gt),
        lambda: check_too_long(pred, gt),
        
        # Semantic patterns
        lambda: check_entity_confusion(pred, gt),
        lambda: check_completely_wrong(pred, gt),
    ]
    
    for check in checks:
        result = check()
        if result is not None:
            return result
    
    # Default: other
    return ErrorPatternResult(
        pattern="other",
        confidence=0.5,
        diagnostics={
            "pred_len": len(pred),
            "gt_len": len(gt),
            "edit_ratio": levenshtein_ratio(pred, gt)
        }
    )


# ==============================================================================
# Counting Utilities
# ==============================================================================

def count_patterns(results: List[ErrorPatternResult]) -> Dict[str, int]:
    """Count occurrences of each pattern."""
    return Counter(r.pattern for r in results)


def count_patterns_with_subpatterns(results: List[ErrorPatternResult]) -> Dict[str, Dict[str, int]]:
    """Count patterns with subpattern breakdown."""
    counts: Dict[str, Dict[str, int]] = {}
    for r in results:
        if r.pattern not in counts:
            counts[r.pattern] = {"_total": 0}
        counts[r.pattern]["_total"] += 1
        if r.subpattern:
            key = r.subpattern
            counts[r.pattern][key] = counts[r.pattern].get(key, 0) + 1
    return counts


def pattern_summary_table(counts: Dict[str, int], total: int) -> List[Dict[str, Any]]:
    """Generate summary table data from pattern counts."""
    rows = []
    for pattern, count in sorted(counts.items(), key=lambda x: -x[1]):
        rows.append({
            "pattern": pattern,
            "count": count,
            "percentage": count / total * 100 if total > 0 else 0
        })
    return rows


# ==============================================================================
# Pattern Descriptions (for documentation)
# ==============================================================================

PATTERN_DESCRIPTIONS = {
    "empty_or_null": "Prediction is empty, None, or null string",
    "severe_truncation": "Prediction is ≤3 chars while GT is much longer",
    "prefix_truncation": "Prediction is a prefix of GT (truncated mid-answer)",
    "too_short_general": "Prediction is significantly shorter than GT",
    "noise_coverage": "Prediction contains GT but has extra noise/tokens",
    "too_long_general": "Prediction is much longer than GT and doesn't contain it",
    "repetition_loop": "Prediction contains repeated tokens/phrases",
    "yes_no_flip": "Yes/No polarity is flipped (yes→no or no→yes)",
    "yes_no_with_explanation": "Correct yes/no answer but with extra explanation",
    "numeric_mismatch": "Both contain numbers but values differ",
    "unit_mismatch": "Same number but different units (%, kg, etc.)",
    "format_numeric": "Same number but different formatting (commas, decimals)",
    "case_punct_only": "Differs only in case or punctuation",
    "whitespace_only": "Differs only in whitespace",
    "char_level_typo": "High character overlap but not exact (OCR-like error)",
    "substring_partial": "Prediction is substring of GT (incomplete)",
    "entity_confusion": "Shares some tokens with GT but key entity differs",
    "completely_wrong": "Completely different from GT (high edit distance)",
    "other": "Uncategorized error",
}


if __name__ == "__main__":
    # Quick test
    print("Testing error_patterns module...")
    
    test_cases = [
        ("yes", "no", ["yes_no_flip"]),
        ("", "hello world", ["empty_or_null"]),
        ("he", "hello world", ["severe_truncation"]),
        ("hello world and more stuff here", "hello", ["noise_coverage"]),
        ("42%", "42 percent", ["unit_mismatch", "format_numeric"]),  # Both valid
        ("HELLO", "hello", ["case_punct_only"]),
        ("apple", "orange", ["completely_wrong"]),
        ("hello hello hello hello", "world", ["repetition_loop"]),
    ]
    
    for pred, gt, expected_list in test_cases:
        result = classify_error_v2(pred, gt)
        status = "✓" if result.pattern in expected_list else "✗"
        expected_str = "/".join(expected_list)
        print(f"{status} pred='{pred}', gt='{gt}' -> {result.pattern} (expected: {expected_str})")

