"""skill_loader.py Progressive Disclosure 기능 단위 테스트.

테스트 대상:
- SkillLevel enum (CORE, STANDARD, EXTENDED)
- classify_skill(name)
- match_skills_by_prompt(prompt, skills_dir)
- get_skills_by_level(skills_dir, level)
- check_security_whitelist(skills_dir)
- set_progressive_disclosure / is_progressive_disclosure_enabled
- list_skills, view_skill, load_skill (회귀 테스트)
"""

from __future__ import annotations

from pathlib import Path

import pytest

from utils.skill_loader import (
    KEYWORD_TRIGGERS,
    SECURITY_WHITELIST,
    SkillDetail,
    SkillLevel,
    SkillSummary,
    check_security_whitelist,
    classify_skill,
    get_skills_by_level,
    is_progressive_disclosure_enabled,
    list_skills,
    load_skill,
    match_skills_by_prompt,
    set_progressive_disclosure,
    view_skill,
)

SKILLS_DIR = "/home/jay/workspace/skills"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture(autouse=True)
def restore_progressive_disclosure():
    """각 테스트가 끝난 뒤 Progressive Disclosure flag를 True(기본값)로 복원한다."""
    yield
    set_progressive_disclosure(True)


# ---------------------------------------------------------------------------
# 1. classify_skill 테스트
# ---------------------------------------------------------------------------


class TestClassifySkill:
    """classify_skill 함수의 CORE / STANDARD / EXTENDED 분류를 검증한다."""

    def test_security_whitelist_skills_are_core(self):
        """SECURITY_WHITELIST에 포함된 스킬은 CORE로 분류되어야 한다."""
        for skill_name in SECURITY_WHITELIST:
            result = classify_skill(skill_name)
            assert result == SkillLevel.CORE, f"'{skill_name}' 은 CORE여야 하지만 {result} 로 분류됨"

    def test_keyword_trigger_skills_are_standard(self):
        """KEYWORD_TRIGGERS에 포함된 스킬은 STANDARD로 분류되어야 한다."""
        for skill_name in KEYWORD_TRIGGERS:
            result = classify_skill(skill_name)
            assert result == SkillLevel.STANDARD, f"'{skill_name}' 은 STANDARD여야 하지만 {result} 로 분류됨"

    def test_unregistered_skill_is_extended(self):
        """미등록 스킬은 EXTENDED로 분류되어야 한다."""
        unregistered_names = [
            "totally-unknown-skill",
            "my-custom-skill",
            "not-a-real-skill-xyz",
        ]
        for name in unregistered_names:
            result = classify_skill(name)
            assert result == SkillLevel.EXTENDED, f"'{name}' 은 EXTENDED여야 하지만 {result} 로 분류됨"

    def test_blog_writer_is_standard(self):
        """blog-writer는 KEYWORD_TRIGGERS에 있으므로 STANDARD여야 한다."""
        assert classify_skill("blog-writer") == SkillLevel.STANDARD

    def test_nuclear_approval_is_core(self):
        """nuclear-approval은 SECURITY_WHITELIST에 있으므로 CORE여야 한다."""
        assert classify_skill("nuclear-approval") == SkillLevel.CORE

    def test_retro_is_standard(self):
        """retro는 KEYWORD_TRIGGERS에 있으므로 STANDARD여야 한다."""
        assert classify_skill("retro") == SkillLevel.STANDARD


# ---------------------------------------------------------------------------
# 2. Feature flag 테스트
# ---------------------------------------------------------------------------


class TestFeatureFlag:
    """Progressive Disclosure feature flag 동작을 검증한다."""

    def test_flag_default_is_enabled(self):
        """기본 상태에서 feature flag가 True여야 한다."""
        set_progressive_disclosure(True)
        assert is_progressive_disclosure_enabled() is True

    def test_flag_off_all_skills_become_core(self):
        """flag가 OFF이면 모든 스킬이 CORE로 분류되어야 한다."""
        set_progressive_disclosure(False)

        assert classify_skill("blog-writer") == SkillLevel.CORE
        assert classify_skill("retro") == SkillLevel.CORE
        assert classify_skill("totally-unknown-skill") == SkillLevel.CORE
        assert classify_skill("nuclear-approval") == SkillLevel.CORE

    def test_flag_on_normal_classification(self):
        """flag가 ON이면 정상적인 3단계 분류가 이루어져야 한다."""
        set_progressive_disclosure(True)

        assert classify_skill("nuclear-approval") == SkillLevel.CORE
        assert classify_skill("blog-writer") == SkillLevel.STANDARD
        assert classify_skill("unknown-xyz") == SkillLevel.EXTENDED

    def test_flag_toggle_restores_classification(self):
        """flag를 OFF→ON으로 토글하면 분류가 원래대로 복원되어야 한다."""
        set_progressive_disclosure(False)
        assert classify_skill("blog-writer") == SkillLevel.CORE

        set_progressive_disclosure(True)
        assert classify_skill("blog-writer") == SkillLevel.STANDARD

    def test_flag_is_restored_after_test(self):
        """autouse fixture가 flag를 True로 복원하는지 검증하기 위해 OFF로 설정 후 확인한다."""
        set_progressive_disclosure(False)
        assert is_progressive_disclosure_enabled() is False
        # fixture가 테스트 종료 후 True로 복원할 것임


# ---------------------------------------------------------------------------
# 3. match_skills_by_prompt 테스트
# ---------------------------------------------------------------------------


class TestMatchSkillsByPrompt:
    """match_skills_by_prompt가 프롬프트 키워드를 정확히 매칭하는지 검증한다."""

    def test_korean_keyword_matches_blog_writer(self):
        """'블로그' 키워드가 포함된 프롬프트는 blog-writer를 매칭해야 한다."""
        results = match_skills_by_prompt("블로그 글 써줘", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert (
            "blog-writer" in matched_names
        ), f"'블로그 글 써줘' 프롬프트에서 blog-writer가 매칭되지 않음. 결과: {matched_names}"

    def test_english_keyword_matches_pdf(self):
        """'pdf' 키워드가 포함된 영어 프롬프트는 pdf를 매칭해야 한다."""
        results = match_skills_by_prompt("help with PDF", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert "pdf" in matched_names, f"'help with PDF' 프롬프트에서 pdf가 매칭되지 않음. 결과: {matched_names}"

    def test_case_insensitive_seo_audit(self):
        """대소문자를 무시하고 'SEO'가 포함된 프롬프트는 seo-audit을 매칭해야 한다."""
        results = match_skills_by_prompt("SEO audit 해줘", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert (
            "seo-audit" in matched_names
        ), f"'SEO audit 해줘' 프롬프트에서 seo-audit이 매칭되지 않음. 결과: {matched_names}"

    def test_no_keyword_returns_empty_list(self):
        """매칭되는 키워드가 없으면 빈 리스트를 반환해야 한다."""
        results = match_skills_by_prompt("아무 키워드도 없는 일반 텍스트", SKILLS_DIR)
        assert results == [], f"매칭 없을 때 빈 리스트여야 함. 결과: {results}"

    def test_multiple_skills_match_simultaneously(self):
        """'블로그 SEO 전략' 프롬프트는 blog-writer와 seo-audit을 동시에 매칭해야 한다."""
        results = match_skills_by_prompt("블로그 SEO 전략", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert (
            "blog-writer" in matched_names
        ), f"'블로그 SEO 전략'에서 blog-writer가 매칭되지 않음. 결과: {matched_names}"
        assert "seo-audit" in matched_names, f"'블로그 SEO 전략'에서 seo-audit이 매칭되지 않음. 결과: {matched_names}"

    def test_result_type_is_list_of_skill_summary(self):
        """반환 타입이 list[SkillSummary]여야 한다."""
        results = match_skills_by_prompt("pdf 변환", SKILLS_DIR)
        assert isinstance(results, list)
        for item in results:
            assert isinstance(item, SkillSummary), f"결과 항목이 SkillSummary가 아님: {type(item)}"

    def test_retro_keyword_match(self):
        """'회고' 키워드가 포함된 프롬프트는 retro를 매칭해야 한다."""
        results = match_skills_by_prompt("이번 주 회고 작성해줘", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert "retro" in matched_names, f"'이번 주 회고 작성해줘'에서 retro가 매칭되지 않음. 결과: {matched_names}"

    def test_lowercase_pdf_keyword(self):
        """소문자 'pdf' 키워드도 pdf 스킬을 매칭해야 한다."""
        results = match_skills_by_prompt("pdf 파일 만들어줘", SKILLS_DIR)
        matched_names = [s.name for s in results]
        assert "pdf" in matched_names


# ---------------------------------------------------------------------------
# 4. check_security_whitelist 테스트
# ---------------------------------------------------------------------------


class TestCheckSecurityWhitelist:
    """check_security_whitelist가 보안 관련 미등록 스킬을 올바르게 감지하는지 검증한다."""

    def test_returns_list_type(self):
        """반환 타입이 list[str]여야 한다."""
        result = check_security_whitelist(SKILLS_DIR)
        assert isinstance(result, list), f"반환 타입이 list여야 함. 실제: {type(result)}"

    def test_all_items_are_strings(self):
        """반환된 목록의 모든 항목이 문자열이어야 한다."""
        result = check_security_whitelist(SKILLS_DIR)
        for item in result:
            assert isinstance(item, str), f"목록 항목이 str이어야 함. 실제: {type(item)}"

    def test_whitelisted_skills_not_in_warnings(self):
        """SECURITY_WHITELIST에 등록된 스킬은 경고 목록에 포함되지 않아야 한다."""
        result = check_security_whitelist(SKILLS_DIR)
        for skill_name in SECURITY_WHITELIST:
            assert skill_name not in result, f"화이트리스트 스킬 '{skill_name}'이 경고 목록에 포함됨"

    def test_nonexistent_dir_returns_empty_list(self):
        """존재하지 않는 디렉토리는 빈 리스트를 반환해야 한다."""
        result = check_security_whitelist("/nonexistent/path/to/skills")
        assert result == []


# ---------------------------------------------------------------------------
# 5. get_skills_by_level 테스트
# ---------------------------------------------------------------------------


class TestGetSkillsByLevel:
    """get_skills_by_level이 레벨별 스킬을 올바르게 반환하는지 검증한다."""

    def test_core_level_includes_security_whitelist_skills(self):
        """CORE 레벨 조회 시 SECURITY_WHITELIST에 있는 스킬(skills 디렉토리에 존재하는 것)이 포함되어야 한다."""
        core_skills = get_skills_by_level(SKILLS_DIR, SkillLevel.CORE)
        core_names = [s.name for s in core_skills]

        # skills/ 디렉토리에 실제로 존재하는 SECURITY_WHITELIST 스킬만 확인
        existing_whitelist = [name for name in SECURITY_WHITELIST if (Path(SKILLS_DIR) / name / "SKILL.md").exists()]
        assert len(existing_whitelist) > 0, "테스트 전제: SECURITY_WHITELIST 스킬이 1개 이상 존재해야 함"

        for skill_name in existing_whitelist:
            assert skill_name in core_names, f"CORE 레벨에 '{skill_name}'이 포함되어야 함. CORE 스킬 목록: {core_names}"

    def test_core_level_returns_list_of_skill_summary(self):
        """CORE 레벨 조회 결과가 list[SkillSummary]여야 한다."""
        result = get_skills_by_level(SKILLS_DIR, SkillLevel.CORE)
        assert isinstance(result, list)
        for item in result:
            assert isinstance(item, SkillSummary)

    def test_standard_level_includes_keyword_trigger_skills(self):
        """STANDARD 레벨에는 KEYWORD_TRIGGERS 스킬 중 skills 디렉토리에 있는 것들이 포함되어야 한다."""
        standard_skills = get_skills_by_level(SKILLS_DIR, SkillLevel.STANDARD)
        standard_names = [s.name for s in standard_skills]

        existing_standard = [name for name in KEYWORD_TRIGGERS if (Path(SKILLS_DIR) / name / "SKILL.md").exists()]
        assert len(existing_standard) > 0, "테스트 전제: KEYWORD_TRIGGERS 스킬이 1개 이상 존재해야 함"

        for skill_name in existing_standard:
            assert skill_name in standard_names, f"STANDARD 레벨에 '{skill_name}'이 포함되어야 함."

    def test_extended_level_excludes_whitelist_and_triggers(self):
        """EXTENDED 레벨에는 SECURITY_WHITELIST나 KEYWORD_TRIGGERS에 포함된 스킬이 없어야 한다."""
        extended_skills = get_skills_by_level(SKILLS_DIR, SkillLevel.EXTENDED)
        for skill in extended_skills:
            assert (
                skill.name not in SECURITY_WHITELIST
            ), f"EXTENDED 레벨에 SECURITY_WHITELIST 스킬 '{skill.name}'이 포함됨"
            assert skill.name not in KEYWORD_TRIGGERS, f"EXTENDED 레벨에 KEYWORD_TRIGGERS 스킬 '{skill.name}'이 포함됨"

    def test_flag_off_all_skills_in_core(self):
        """flag가 OFF이면 get_skills_by_level(CORE)이 모든 스킬을 반환해야 한다."""
        set_progressive_disclosure(False)
        all_skills = list_skills(SKILLS_DIR)
        core_skills = get_skills_by_level(SKILLS_DIR, SkillLevel.CORE)
        assert len(core_skills) == len(
            all_skills
        ), f"flag OFF 시 CORE 스킬 수({len(core_skills)})가 전체 스킬 수({len(all_skills)})와 같아야 함"

    def test_flag_off_standard_and_extended_empty(self):
        """flag가 OFF이면 STANDARD와 EXTENDED 레벨은 빈 리스트를 반환해야 한다."""
        set_progressive_disclosure(False)
        assert get_skills_by_level(SKILLS_DIR, SkillLevel.STANDARD) == []
        assert get_skills_by_level(SKILLS_DIR, SkillLevel.EXTENDED) == []


# ---------------------------------------------------------------------------
# 6. 기존 함수 회귀 테스트
# ---------------------------------------------------------------------------


class TestLegacyFunctions:
    """list_skills, view_skill, load_skill의 기존 동작을 검증한다."""

    def test_list_skills_returns_list(self):
        """list_skills 호출 시 결과가 리스트여야 한다."""
        result = list_skills(SKILLS_DIR)
        assert isinstance(result, list), f"list_skills 결과가 list여야 함. 실제: {type(result)}"

    def test_list_skills_not_empty(self):
        """실제 skills 디렉토리에서 list_skills 결과가 비어 있지 않아야 한다."""
        result = list_skills(SKILLS_DIR)
        assert len(result) > 0, "skills 디렉토리에 스킬이 1개 이상 있어야 함"

    def test_list_skills_contains_skill_summary_instances(self):
        """list_skills 결과의 각 항목이 SkillSummary 인스턴스여야 한다."""
        result = list_skills(SKILLS_DIR)
        for item in result:
            assert isinstance(item, SkillSummary), f"항목이 SkillSummary여야 함. 실제: {type(item)}"

    def test_view_skill_retro_returns_skill_detail(self):
        """view_skill('retro') 호출 시 SkillDetail 인스턴스를 반환해야 한다."""
        result = view_skill(SKILLS_DIR, "retro")
        assert isinstance(result, SkillDetail), f"view_skill('retro') 결과가 SkillDetail이어야 함. 실제: {type(result)}"

    def test_view_skill_detail_has_required_fields(self):
        """view_skill('retro') 결과에 name, description, triggers, requires 필드가 있어야 한다."""
        detail = view_skill(SKILLS_DIR, "retro")
        assert hasattr(detail, "name")
        assert hasattr(detail, "description")
        assert hasattr(detail, "triggers")
        assert hasattr(detail, "requires")
        assert hasattr(detail, "full_path")

    def test_load_skill_pdf_returns_string(self):
        """load_skill('pdf') 호출 시 문자열을 반환해야 한다."""
        result = load_skill(SKILLS_DIR, "pdf")
        assert isinstance(result, str), f"load_skill('pdf') 결과가 str이어야 함. 실제: {type(result)}"

    def test_load_skill_pdf_not_empty(self):
        """load_skill('pdf')의 반환 문자열이 비어 있지 않아야 한다."""
        result = load_skill(SKILLS_DIR, "pdf")
        assert len(result) > 0, "load_skill('pdf') 결과가 비어 있음"

    def test_view_skill_nonexistent_raises_file_not_found(self):
        """존재하지 않는 스킬 이름으로 view_skill 호출 시 FileNotFoundError가 발생해야 한다."""
        with pytest.raises(FileNotFoundError):
            view_skill(SKILLS_DIR, "nonexistent-skill-xyz-123")

    def test_load_skill_nonexistent_raises_file_not_found(self):
        """존재하지 않는 스킬 이름으로 load_skill 호출 시 FileNotFoundError가 발생해야 한다."""
        with pytest.raises(FileNotFoundError):
            load_skill(SKILLS_DIR, "nonexistent-skill-xyz-123")

    def test_list_skills_nonexistent_dir_returns_empty(self):
        """존재하지 않는 디렉토리로 list_skills 호출 시 빈 리스트를 반환해야 한다."""
        result = list_skills("/nonexistent/path/xyz")
        assert result == []

    def test_view_skill_blog_writer_returns_skill_detail(self):
        """view_skill('blog-writer') 호출 시 SkillDetail 인스턴스를 반환해야 한다."""
        result = view_skill(SKILLS_DIR, "blog-writer")
        assert isinstance(result, SkillDetail)

    def test_load_skill_retro_returns_string(self):
        """load_skill('retro') 호출 시 문자열을 반환해야 한다."""
        result = load_skill(SKILLS_DIR, "retro")
        assert isinstance(result, str)
        assert len(result) > 0
