# pyright: reportMissingImports=false
"""
models_v2 단위 테스트
테스터: 모리건 (개발3팀)
"""
from kakao_knowledge.models_v2 import (
    InsightType,
    InsightV2,
    ThreadV2,
    Stage1Result,
    CATEGORIES,
    ALL_CATEGORIES,
)
from kakao_knowledge.models import ChatMessage, MessageType


# ---------------------------------------------------------------------------
# 1. InsightType enum
# ---------------------------------------------------------------------------


class TestInsightTypeEnum:
    """InsightType enum 검증"""

    def test_enum_has_seven_values(self):
        """InsightType enum에 7개의 값이 존재해야 한다"""
        assert len(InsightType) == 7, f"InsightType 값 개수가 7개여야 하지만 {len(InsightType)}개임"

    def test_enum_values_are_correct(self):
        """InsightType 7개 값이 모두 올바르게 정의돼야 한다"""
        expected = {
            "qa",
            "expert_opinion",
            "case_analysis",
            "regulation_interpretation",
            "practical_tip",
            "regulation_change",
            "warning",
        }
        actual = {e.value for e in InsightType}
        assert actual == expected, f"InsightType 값 불일치: {actual - expected} 초과, {expected - actual} 누락"

    def test_enum_is_str_subclass(self):
        """InsightType은 str 서브클래스여야 한다 (JSON 직렬화 호환)"""
        assert isinstance(InsightType.QA, str), "InsightType.QA가 str 인스턴스가 아님"

    def test_qa_value(self):
        assert InsightType.QA == "qa"

    def test_expert_opinion_value(self):
        assert InsightType.EXPERT_OPINION == "expert_opinion"

    def test_warning_value(self):
        assert InsightType.WARNING == "warning"

    def test_regulation_change_value(self):
        assert InsightType.REGULATION_CHANGE == "regulation_change"


# ---------------------------------------------------------------------------
# 2. InsightV2 필수 필드
# ---------------------------------------------------------------------------


class TestInsightV2RequiredFields:
    """InsightV2 필수 필드 존재 확인"""

    def _make_minimal(self) -> InsightV2:
        return InsightV2(
            id="insight-001",
            title="테스트 인사이트",
            type=InsightType.QA,
            category="보상",
            summary="요약 내용입니다.",
            key_points=["포인트1", "포인트2"],
        )

    def test_id_field_exists(self):
        """id 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "id"), "InsightV2에 id 필드가 없음"

    def test_title_field_exists(self):
        """title 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "title"), "InsightV2에 title 필드가 없음"

    def test_type_field_exists(self):
        """type 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "type"), "InsightV2에 type 필드가 없음"

    def test_category_field_exists(self):
        """category 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "category"), "InsightV2에 category 필드가 없음"

    def test_summary_field_exists(self):
        """summary 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "summary"), "InsightV2에 summary 필드가 없음"

    def test_key_points_field_exists(self):
        """key_points 필드가 존재해야 한다"""
        obj = self._make_minimal()
        assert hasattr(obj, "key_points"), "InsightV2에 key_points 필드가 없음"


# ---------------------------------------------------------------------------
# 3. InsightV2 기본값 확인
# ---------------------------------------------------------------------------


class TestInsightV2DefaultValues:
    """InsightV2 기본값 설정 확인"""

    def _make_minimal(self) -> InsightV2:
        return InsightV2(
            id="insight-001",
            title="기본값 테스트",
            type=InsightType.PRACTICAL_TIP,
            category="영업노하우",
            summary="요약",
            key_points=[],
        )

    def test_expert_defaults_to_empty_string(self):
        """expert 기본값은 빈 문자열이어야 한다"""
        obj = self._make_minimal()
        assert obj.expert == "", f"expert 기본값이 빈 문자열이 아님: {repr(obj.expert)}"

    def test_confidence_defaults_to_medium(self):
        """confidence 기본값은 'medium'이어야 한다"""
        obj = self._make_minimal()
        assert obj.confidence == "medium", f"confidence 기본값이 medium이 아님: {repr(obj.confidence)}"

    def test_related_topics_defaults_to_empty_list(self):
        """related_topics 기본값은 빈 리스트여야 한다"""
        obj = self._make_minimal()
        assert obj.related_topics == [], f"related_topics 기본값이 빈 리스트가 아님"

    def test_tags_defaults_to_empty_list(self):
        """tags 기본값은 빈 리스트여야 한다"""
        obj = self._make_minimal()
        assert obj.tags == [], f"tags 기본값이 빈 리스트가 아님"

    def test_source_date_defaults_to_empty_string(self):
        """source_date 기본값은 빈 문자열이어야 한다"""
        obj = self._make_minimal()
        assert obj.source_date == "", f"source_date 기본값 오류: {repr(obj.source_date)}"

    def test_source_chat_defaults_to_empty_string(self):
        """source_chat 기본값은 빈 문자열이어야 한다"""
        obj = self._make_minimal()
        assert obj.source_chat == "", f"source_chat 기본값 오류: {repr(obj.source_chat)}"

    def test_raw_thread_defaults_to_empty_list(self):
        """raw_thread 기본값은 빈 리스트여야 한다"""
        obj = self._make_minimal()
        assert obj.raw_thread == [], f"raw_thread 기본값이 빈 리스트가 아님"

    def test_participants_defaults_to_empty_list(self):
        """participants 기본값은 빈 리스트여야 한다"""
        obj = self._make_minimal()
        assert obj.participants == [], f"participants 기본값이 빈 리스트가 아님"

    def test_question_defaults_to_empty_string(self):
        """question 기본값은 빈 문자열이어야 한다"""
        obj = self._make_minimal()
        assert obj.question == "", f"question 기본값 오류: {repr(obj.question)}"

    def test_answer_defaults_to_empty_string(self):
        """answer 기본값은 빈 문자열이어야 한다"""
        obj = self._make_minimal()
        assert obj.answer == "", f"answer 기본값 오류: {repr(obj.answer)}"


# ---------------------------------------------------------------------------
# 4. InsightV2 유효한 데이터로 생성 가능
# ---------------------------------------------------------------------------


class TestInsightV2Creation:
    """InsightV2 생성 가능 확인"""

    def test_create_with_minimal_fields(self):
        """필수 필드만으로 생성 가능해야 한다"""
        obj = InsightV2(
            id="insight-001",
            title="광응고술 수술 해당 여부",
            type=InsightType.QA,
            category="보상",
            summary="광응고술은 약관 개정으로 수술에 해당합니다.",
            key_points=["약관 개정", "레이저 수술 포함"],
        )
        assert obj.id == "insight-001", "id 값 불일치"
        assert obj.title == "광응고술 수술 해당 여부", "title 값 불일치"
        assert obj.type == InsightType.QA, "type 값 불일치"

    def test_create_with_all_fields(self):
        """모든 필드로 생성 가능해야 한다"""
        obj = InsightV2(
            id="insight-002",
            title="비만약 고지의무 면책 사례",
            type=InsightType.CASE_ANALYSIS,
            category="고지의무",
            summary="비만약 처방 미고지로 면책된 사례가 발생했습니다.",
            key_points=["비만약", "고지의무 위반", "면책"],
            expert="이해철/프라임/부산",
            confidence="high",
            related_topics=["고지의무", "면책"],
            tags=["#비만약", "#고지의무"],
            source_date="2025-12-03",
            source_chat="보험설계사 커뮤니티",
            raw_thread=["[이해철] 비만약 면책 사례가 나왔습니다"],
            participants=["이해철/프라임/부산", "박유진/인카/서울"],
            question="",
            answer="",
        )
        assert obj.expert == "이해철/프라임/부산", "expert 값 불일치"
        assert obj.confidence == "high", "confidence 값 불일치"
        assert len(obj.key_points) == 3, "key_points 개수 불일치"

    def test_type_accepts_all_enum_values(self):
        """모든 InsightType enum 값으로 생성 가능해야 한다"""
        for insight_type in InsightType:
            obj = InsightV2(
                id="insight-001",
                title="테스트",
                type=insight_type,
                category="기타",
                summary="요약",
                key_points=[],
            )
            assert obj.type == insight_type, f"type={insight_type} 생성 실패"


# ---------------------------------------------------------------------------
# 5. InsightV2 model_dump() 동작 확인
# ---------------------------------------------------------------------------


class TestInsightV2ModelDump:
    """InsightV2.model_dump() 동작 확인"""

    def _make_insight(self) -> InsightV2:
        return InsightV2(
            id="insight-001",
            title="테스트 인사이트",
            type=InsightType.EXPERT_OPINION,
            category="고지의무",
            summary="전문가 의견 요약",
            key_points=["포인트1"],
            expert="이해철/프라임/부산",
            confidence="high",
        )

    def test_model_dump_returns_dict(self):
        """model_dump()는 dict를 반환해야 한다"""
        obj = self._make_insight()
        result = obj.model_dump()
        assert isinstance(result, dict), f"model_dump() 반환 타입이 dict가 아님: {type(result)}"

    def test_model_dump_contains_id(self):
        """model_dump() 결과에 id 키가 있어야 한다"""
        obj = self._make_insight()
        result = obj.model_dump()
        assert "id" in result, "model_dump() 결과에 id 키 없음"
        assert result["id"] == "insight-001", f"id 값 불일치: {result['id']}"

    def test_model_dump_type_is_string(self):
        """model_dump() 결과의 type은 문자열이어야 한다 (InsightType이 str 서브클래스)"""
        obj = self._make_insight()
        result = obj.model_dump()
        assert isinstance(result["type"], str), f"type이 str이 아님: {type(result['type'])}"

    def test_model_dump_contains_all_required_keys(self):
        """model_dump() 결과에 필수 키가 모두 있어야 한다"""
        required_keys = {
            "id", "title", "type", "category", "summary", "key_points",
            "expert", "confidence", "related_topics", "tags",
            "source_date", "source_chat", "raw_thread", "participants",
            "question", "answer",
        }
        obj = self._make_insight()
        result = obj.model_dump()
        missing = required_keys - set(result.keys())
        assert not missing, f"model_dump() 결과에 필수 키 누락: {missing}"

    def test_model_dump_key_points_is_list(self):
        """model_dump() 결과의 key_points는 리스트여야 한다"""
        obj = self._make_insight()
        result = obj.model_dump()
        assert isinstance(result["key_points"], list), "key_points가 리스트가 아님"


# ---------------------------------------------------------------------------
# 6. ThreadV2 생성 + 기본값 확인
# ---------------------------------------------------------------------------


class TestThreadV2:
    """ThreadV2 생성 및 기본값 확인"""

    def test_create_empty_thread(self):
        """빈 ThreadV2 생성 가능해야 한다"""
        thread = ThreadV2()
        assert thread.messages == [], "messages 기본값이 빈 리스트가 아님"

    def test_start_time_defaults_to_empty_string(self):
        """start_time 기본값은 빈 문자열이어야 한다"""
        thread = ThreadV2()
        assert thread.start_time == "", f"start_time 기본값 오류: {repr(thread.start_time)}"

    def test_topic_label_defaults_to_empty_string(self):
        """topic_label 기본값은 빈 문자열이어야 한다"""
        thread = ThreadV2()
        assert thread.topic_label == "", f"topic_label 기본값 오류: {repr(thread.topic_label)}"

    def test_has_insight_defaults_to_none(self):
        """has_insight 기본값은 None이어야 한다"""
        thread = ThreadV2()
        assert thread.has_insight is None, f"has_insight 기본값 오류: {thread.has_insight}"

    def test_insight_types_defaults_to_empty_list(self):
        """insight_types 기본값은 빈 리스트여야 한다"""
        thread = ThreadV2()
        assert thread.insight_types == [], f"insight_types 기본값이 빈 리스트가 아님"

    def test_create_with_messages(self):
        """메시지를 포함한 ThreadV2 생성 가능해야 한다"""
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="질문입니다",
                type=MessageType.MESSAGE,
            ),
        ]
        thread = ThreadV2(messages=messages, start_time="2025-12-03 17:00")
        assert len(thread.messages) == 1, "메시지 개수 불일치"
        assert thread.start_time == "2025-12-03 17:00", "start_time 값 불일치"

    def test_set_has_insight_true(self):
        """has_insight를 True로 설정 가능해야 한다"""
        thread = ThreadV2(has_insight=True, insight_types=["qa"])
        assert thread.has_insight is True
        assert "qa" in thread.insight_types

    def test_insight_types_isolation(self):
        """ThreadV2 인스턴스 간 insight_types 리스트가 공유되지 않아야 한다 (dataclass field 격리)"""
        t1 = ThreadV2()
        t2 = ThreadV2()
        t1.insight_types.append("qa")
        assert t2.insight_types == [], "ThreadV2 인스턴스 간 insight_types 리스트가 공유됨 (dataclass field 버그)"


# ---------------------------------------------------------------------------
# 7. Stage1Result 생성 확인
# ---------------------------------------------------------------------------


class TestStage1Result:
    """Stage1Result 생성 및 필드 확인"""

    def _make_thread(self) -> ThreadV2:
        return ThreadV2(
            messages=[
                ChatMessage(
                    date="2025-12-03",
                    time="17:00",
                    user="질문자/인카/서울",
                    content="#궁금증\n질문입니다",
                    type=MessageType.MESSAGE,
                ),
            ],
            start_time="2025-12-03 17:00",
        )

    def test_create_with_has_insight_true(self):
        """has_insight=True로 Stage1Result 생성 가능해야 한다"""
        thread = self._make_thread()
        result = Stage1Result(
            thread=thread,
            has_insight=True,
            insight_types=["qa"],
        )
        assert result.has_insight is True, "has_insight 값 불일치"
        assert result.insight_types == ["qa"], "insight_types 값 불일치"

    def test_create_with_has_insight_false(self):
        """has_insight=False + noise_reason으로 생성 가능해야 한다"""
        thread = self._make_thread()
        result = Stage1Result(
            thread=thread,
            has_insight=False,
            insight_types=[],
            noise_reason="단순 인사 메시지",
        )
        assert result.has_insight is False
        assert result.noise_reason == "단순 인사 메시지", "noise_reason 값 불일치"

    def test_noise_reason_defaults_to_empty_string(self):
        """noise_reason 기본값은 빈 문자열이어야 한다"""
        thread = self._make_thread()
        result = Stage1Result(thread=thread, has_insight=True, insight_types=[])
        assert result.noise_reason == "", f"noise_reason 기본값 오류: {repr(result.noise_reason)}"

    def test_thread_field_is_thread_v2(self):
        """thread 필드가 ThreadV2 인스턴스여야 한다"""
        thread = self._make_thread()
        result = Stage1Result(thread=thread, has_insight=True, insight_types=["expert_opinion"])
        assert isinstance(result.thread, ThreadV2), "thread 필드가 ThreadV2가 아님"

    def test_insight_types_multiple_values(self):
        """insight_types에 여러 값이 저장 가능해야 한다"""
        thread = self._make_thread()
        result = Stage1Result(
            thread=thread,
            has_insight=True,
            insight_types=["qa", "expert_opinion"],
        )
        assert len(result.insight_types) == 2, "insight_types 복수 값 저장 실패"


# ---------------------------------------------------------------------------
# 8. CATEGORIES dict 확인
# ---------------------------------------------------------------------------


class TestCategoriesDict:
    """CATEGORIES dict 검증"""

    def test_categories_has_at_least_15_entries(self):
        """CATEGORIES dict에 15개 이상의 카테고리가 있어야 한다"""
        count = len(CATEGORIES)
        assert count >= 15, f"CATEGORIES 카테고리 수가 15개 미만: {count}개"

    def test_categories_is_dict(self):
        """CATEGORIES는 dict 타입이어야 한다"""
        assert isinstance(CATEGORIES, dict), f"CATEGORIES가 dict가 아님: {type(CATEGORIES)}"

    def test_categories_contains_보상(self):
        """CATEGORIES에 '보상' 카테고리가 있어야 한다"""
        assert "보상" in CATEGORIES, "CATEGORIES에 '보상' 카테고리 없음"

    def test_categories_contains_고지의무(self):
        """CATEGORIES에 '고지의무' 카테고리가 있어야 한다"""
        assert "고지의무" in CATEGORIES, "CATEGORIES에 '고지의무' 카테고리 없음"

    def test_categories_contains_약관해석(self):
        """CATEGORIES에 '약관해석' 카테고리가 있어야 한다"""
        assert "약관해석" in CATEGORIES, "CATEGORIES에 '약관해석' 카테고리 없음"

    def test_categories_contains_상품비교(self):
        """CATEGORIES에 '상품비교' 카테고리가 있어야 한다"""
        assert "상품비교" in CATEGORIES, "CATEGORIES에 '상품비교' 카테고리 없음"

    def test_categories_values_are_lists(self):
        """CATEGORIES의 각 값은 리스트여야 한다"""
        for key, value in CATEGORIES.items():
            assert isinstance(value, list), f"CATEGORIES['{key}'] 값이 리스트가 아님: {type(value)}"


# ---------------------------------------------------------------------------
# 9. ALL_CATEGORIES 리스트 확인
# ---------------------------------------------------------------------------


class TestAllCategories:
    """ALL_CATEGORIES 리스트 검증"""

    def test_all_categories_is_not_empty(self):
        """ALL_CATEGORIES가 비어있지 않아야 한다"""
        assert len(ALL_CATEGORIES) > 0, "ALL_CATEGORIES가 비어있음"

    def test_all_categories_is_list(self):
        """ALL_CATEGORIES는 리스트여야 한다"""
        assert isinstance(ALL_CATEGORIES, list), f"ALL_CATEGORIES가 리스트가 아님: {type(ALL_CATEGORIES)}"

    def test_all_categories_contains_strings(self):
        """ALL_CATEGORIES의 모든 항목이 문자열이어야 한다"""
        for item in ALL_CATEGORIES:
            assert isinstance(item, str), f"ALL_CATEGORIES에 문자열이 아닌 항목 있음: {repr(item)}"

    def test_all_categories_contains_보상_subpath(self):
        """ALL_CATEGORIES에 '보상/자동차' 같은 서브카테고리가 포함되어야 한다"""
        # 보상은 서브카테고리("자동차", "일반", "장기")가 있으므로 "보상/자동차" 형식이어야 함
        has_보상_sub = any("보상/" in cat for cat in ALL_CATEGORIES)
        assert has_보상_sub, "ALL_CATEGORIES에 '보상/' 서브카테고리가 없음"

    def test_all_categories_no_duplicates(self):
        """ALL_CATEGORIES에 중복 항목이 없어야 한다"""
        assert len(ALL_CATEGORIES) == len(set(ALL_CATEGORIES)), "ALL_CATEGORIES에 중복 항목이 있음"
