#!/usr/bin/env python3
"""utils/fuzzy_match.py 테스트 스위트"""

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.fuzzy_match import (
    fuzzy_match,
    levenshtein_distance,
    similarity_ratio,
)


class TestLevenshteinDistance:
    """levenshtein_distance() 테스트"""

    def test_identical_strings(self) -> None:
        """동일 문자열 → 거리 0"""
        assert levenshtein_distance("hello", "hello") == 0

    def test_empty_strings(self) -> None:
        """빈 문자열 간 거리"""
        assert levenshtein_distance("", "") == 0

    def test_one_empty(self) -> None:
        """한쪽이 빈 문자열"""
        assert levenshtein_distance("abc", "") == 3
        assert levenshtein_distance("", "xyz") == 3

    def test_single_insertion(self) -> None:
        """1자 삽입"""
        assert levenshtein_distance("cat", "cats") == 1

    def test_single_deletion(self) -> None:
        """1자 삭제"""
        assert levenshtein_distance("cats", "cat") == 1

    def test_single_substitution(self) -> None:
        """1자 치환"""
        assert levenshtein_distance("cat", "bat") == 1

    def test_complex_edit(self) -> None:
        """복합 편집"""
        assert levenshtein_distance("kitten", "sitting") == 3

    def test_completely_different(self) -> None:
        """완전히 다른 문자열"""
        assert levenshtein_distance("abc", "xyz") == 3

    def test_symmetry(self) -> None:
        """대칭성: d(a,b) == d(b,a)"""
        assert levenshtein_distance("saturday", "sunday") == levenshtein_distance(
            "sunday", "saturday"
        )

    def test_returns_int(self) -> None:
        """반환 타입은 int"""
        result = levenshtein_distance("foo", "bar")
        assert isinstance(result, int)

    def test_non_negative(self) -> None:
        """항상 비음수"""
        assert levenshtein_distance("test", "text") >= 0


class TestSimilarityRatio:
    """similarity_ratio() 테스트"""

    def test_identical_strings(self) -> None:
        """동일 문자열 → 1.0"""
        assert similarity_ratio("hello", "hello") == 1.0

    def test_empty_strings(self) -> None:
        """둘 다 빈 문자열 → 1.0"""
        assert similarity_ratio("", "") == 1.0

    def test_completely_different(self) -> None:
        """완전히 다른 문자열 → 낮은 유사도"""
        ratio = similarity_ratio("abc", "xyz")
        assert 0.0 <= ratio < 0.5

    def test_range_0_to_1(self) -> None:
        """결과는 0.0~1.0 범위"""
        ratio = similarity_ratio("cat", "car")
        assert 0.0 <= ratio <= 1.0

    def test_high_similarity(self) -> None:
        """비슷한 문자열은 높은 유사도"""
        ratio = similarity_ratio("hello", "helo")
        assert ratio > 0.7

    def test_returns_float(self) -> None:
        """반환 타입은 float"""
        result = similarity_ratio("foo", "bar")
        assert isinstance(result, float)

    def test_one_empty_string(self) -> None:
        """한쪽 빈 문자열 → 0.0"""
        assert similarity_ratio("hello", "") == 0.0
        assert similarity_ratio("", "world") == 0.0

    def test_symmetry(self) -> None:
        """대칭성: ratio(a,b) == ratio(b,a)"""
        assert similarity_ratio("python", "typhon") == similarity_ratio(
            "typhon", "python"
        )


class TestFuzzyMatch:
    """fuzzy_match() 테스트"""

    def test_exact_match(self) -> None:
        """정확히 일치하는 항목은 1.0 점수"""
        results = fuzzy_match("hello", ["hello", "world", "foo"])
        assert len(results) >= 1
        best_name, best_score = results[0]
        assert best_name == "hello"
        assert best_score == 1.0

    def test_returns_list_of_tuples(self) -> None:
        """반환 타입: list[tuple[str, float]]"""
        results = fuzzy_match("cat", ["cat", "car", "bat"])
        assert isinstance(results, list)
        for item in results:
            assert isinstance(item, tuple)
            assert len(item) == 2
            name, score = item
            assert isinstance(name, str)
            assert isinstance(score, float)

    def test_threshold_filtering(self) -> None:
        """threshold 이하 결과 제외"""
        candidates = ["hello", "world", "completely_different_xyz_abc"]
        results = fuzzy_match("hello", candidates, threshold=0.8)
        # "completely_different_xyz_abc"는 threshold 미만이어야 함
        names = [name for name, _ in results]
        assert "completely_different_xyz_abc" not in names

    def test_max_results_limit(self) -> None:
        """max_results 개수 제한"""
        candidates = [f"item_{i}" for i in range(20)]
        results = fuzzy_match("item_5", candidates, max_results=3)
        assert len(results) <= 3

    def test_sorted_by_score_descending(self) -> None:
        """결과는 점수 내림차순 정렬"""
        results = fuzzy_match("python", ["python", "typhon", "java", "ython"])
        scores = [score for _, score in results]
        assert scores == sorted(scores, reverse=True)

    def test_empty_candidates(self) -> None:
        """빈 후보 목록 → 빈 결과"""
        results = fuzzy_match("hello", [])
        assert results == []

    def test_no_matches_above_threshold(self) -> None:
        """threshold 이상 없으면 빈 결과"""
        results = fuzzy_match("hello", ["xyz", "abc", "qqq"], threshold=0.9)
        assert results == []

    def test_score_between_0_and_1(self) -> None:
        """모든 점수는 0.0~1.0"""
        results = fuzzy_match("test", ["test", "text", "testing", "best"])
        for _, score in results:
            assert 0.0 <= score <= 1.0

    def test_default_threshold(self) -> None:
        """기본 threshold=0.6 동작 확인"""
        results = fuzzy_match("calculate", ["calculate", "calculation", "calc"])
        # 충분히 유사한 항목은 포함
        names = [name for name, _ in results]
        assert "calculate" in names

    def test_default_max_results(self) -> None:
        """기본 max_results=5 제한"""
        candidates = [f"file_{i:03d}.py" for i in range(100)]
        results = fuzzy_match("file_005.py", candidates)
        assert len(results) <= 5

    def test_case_sensitivity(self) -> None:
        """대소문자 구별 여부 확인 (기본 동작)"""
        results_lower = fuzzy_match("hello", ["hello", "HELLO"])
        # 소문자 "hello"가 최고 점수여야 함
        assert results_lower[0][0] == "hello"

    def test_partial_match_included(self) -> None:
        """부분 일치도 threshold 이상이면 포함"""
        results = fuzzy_match("calc", ["calculate", "calculator", "calibrate"])
        # threshold 기본 0.6으로, 최소 하나는 포함될 수 있음
        assert isinstance(results, list)

    def test_single_candidate(self) -> None:
        """후보 1개"""
        results = fuzzy_match("hello", ["hello"], threshold=0.5)
        assert len(results) == 1
        assert results[0][0] == "hello"
        assert results[0][1] == 1.0
