# pyright: reportMissingImports=false
"""
knowledge_extractor 단위 테스트
테스터: 모리건 (개발3팀)
"""
from kakao_knowledge.knowledge_extractor import extract_knowledge
from kakao_knowledge.models import ChatMessage, MessageType

# ---------------------------------------------------------------------------
# 필수 키 목록
# ---------------------------------------------------------------------------

REQUIRED_KEYS = {
    "id",
    "title",
    "category",
    "subcategory",
    "question",
    "answer",
    "expert",
    "source_date",
    "source_chat",
    "keywords",
    "confidence",
    "raw_thread",
}


# ---------------------------------------------------------------------------
# 1. 스레드 분리: #궁금증 태그 기반
# ---------------------------------------------------------------------------


class TestThreadSplitByTag:
    def test_single_entry_created(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="#궁금증\n• 유형 : 보상\n• 질문내용 :\n광응고술이 수술에 해당하나요?",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="이해철/프라임/부산",
                content="네 수술에 해당합니다.",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:06",
                user="이해철/프라임/부산",
                content="약관이 개정되어서 명시되어 있습니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 1

    def test_category_is_보상(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="#궁금증\n• 유형 : 보상\n• 질문내용 :\n광응고술이 수술에 해당하나요?",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="이해철/프라임/부산",
                content="네 수술에 해당합니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert result[0]["category"] == "보상"

    def test_question_contains_광응고술(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="#궁금증\n• 유형 : 보상\n• 질문내용 :\n광응고술이 수술에 해당하나요?",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="이해철/프라임/부산",
                content="네 수술에 해당합니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert "광응고술" in result[0]["question"]

    def test_answer_contains_수술에_해당(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="#궁금증\n• 유형 : 보상\n• 질문내용 :\n광응고술이 수술에 해당하나요?",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="이해철/프라임/부산",
                content="네 수술에 해당합니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert "수술에 해당" in result[0]["answer"]


# ---------------------------------------------------------------------------
# 2. 스레드 분리: 30분 gap
# ---------------------------------------------------------------------------


class TestThreadSplitBy30MinGap:
    def _make_messages(self):
        return [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n• 유형 : 보상\n질문1",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:10",
                user="D/E/F",
                content="답변1",
                type=MessageType.MESSAGE,
            ),
            # 30분 이상 gap
            ChatMessage(
                date="2025-12-03",
                time="18:00",
                user="G/H/I",
                content="#궁금증\n• 유형 : 약관\n질문2",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="18:05",
                user="D/E/F",
                content="답변2",
                type=MessageType.MESSAGE,
            ),
        ]

    def test_two_entries_created(self):
        result = extract_knowledge(self._make_messages())
        assert len(result) == 2

    def test_first_entry_category(self):
        result = extract_knowledge(self._make_messages())
        assert result[0]["category"] == "보상"

    def test_second_entry_category(self):
        result = extract_knowledge(self._make_messages())
        assert result[1]["category"] == "약관"


# ---------------------------------------------------------------------------
# 3. 날짜 변경 → 새 스레드
# ---------------------------------------------------------------------------


class TestThreadSplitByDateChange:
    def _make_messages(self):
        return [
            ChatMessage(
                date="2025-12-03",
                time="23:50",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="23:55",
                user="D/E/F",
                content="답변",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-04",
                time="00:05",
                user="G/H/I",
                content="#궁금증\n새 질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-04",
                time="00:10",
                user="D/E/F",
                content="새 답변",
                type=MessageType.MESSAGE,
            ),
        ]

    def test_two_entries_created(self):
        result = extract_knowledge(self._make_messages())
        assert len(result) == 2

    def test_first_entry_source_date(self):
        result = extract_knowledge(self._make_messages())
        assert result[0]["source_date"] == "2025-12-03"

    def test_second_entry_source_date(self):
        result = extract_knowledge(self._make_messages())
        assert result[1]["source_date"] == "2025-12-04"


# ---------------------------------------------------------------------------
# 4. 입퇴장/미디어 메시지 제외
# ---------------------------------------------------------------------------


class TestNonMessageTypeExclusion:
    def _make_messages(self):
        return [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:02",
                user="X님",
                content="X님이 들어왔습니다.",
                type=MessageType.JOIN,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:03",
                user="D/E/F",
                content="사진",
                type=MessageType.PHOTO,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]

    def test_one_entry_created(self):
        result = extract_knowledge(self._make_messages())
        assert len(result) == 1

    def test_raw_thread_excludes_join(self):
        result = extract_knowledge(self._make_messages())
        raw = result[0]["raw_thread"]
        for line in raw:
            assert "들어왔습니다" not in line

    def test_raw_thread_excludes_photo(self):
        result = extract_knowledge(self._make_messages())
        raw = result[0]["raw_thread"]
        # "사진" 단독 항목이 없어야 함 (내용이 "사진"인 PHOTO 타입)
        for line in raw:
            assert line.strip() != "사진"


# ---------------------------------------------------------------------------
# 5. 카테고리 추출
# ---------------------------------------------------------------------------


class TestCategoryExtraction:
    def _entry_for_content(self, content: str) -> dict:
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content=content,
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 1
        return result[0]

    def test_category_보상(self):
        entry = self._entry_for_content("#궁금증\n• 유형 : 보상\n질문")
        assert entry["category"] == "보상"

    def test_category_고지의무(self):
        entry = self._entry_for_content("#궁금증\n• 유형 : 고지의무\n질문")
        assert entry["category"] == "고지의무"

    def test_category_약관(self):
        entry = self._entry_for_content("#궁금증\n• 유형 : 약관\n질문")
        assert entry["category"] == "약관"

    def test_category_상품(self):
        entry = self._entry_for_content("#궁금증\n• 유형 : 상품\n질문")
        assert entry["category"] == "상품"

    def test_category_기타_when_no_type(self):
        entry = self._entry_for_content("#궁금증\n유형 없이 질문만")
        assert entry["category"] == "기타"


# ---------------------------------------------------------------------------
# 6. expert 필드: 가장 많이 답변한 사용자
# ---------------------------------------------------------------------------


class TestExpertField:
    def test_expert_is_most_frequent_responder(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="질문자/인카/서울",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="전문가A/프라임/부산",
                content="답변1",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:06",
                user="전문가A/프라임/부산",
                content="답변2",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:07",
                user="일반인B/KB/대전",
                content="답변3",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert result[0]["expert"] == "전문가A/프라임/부산"


# ---------------------------------------------------------------------------
# 7. 전화번호 마스킹
# ---------------------------------------------------------------------------


class TestPhoneNumberMasking:
    def test_phone_number_masked_in_raw_thread(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="문의는 010-1234-5678로 연락주세요.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        raw = result[0]["raw_thread"]
        full_text = "\n".join(raw)
        assert "010-1234-5678" not in full_text
        assert "***-****-****" in full_text


# ---------------------------------------------------------------------------
# 8. 최소 메시지 수: 단일 메시지 스레드 제외
# ---------------------------------------------------------------------------


class TestMinimumMessageCount:
    def test_single_message_thread_excluded(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문만 있고 답변 없음",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 0


# ---------------------------------------------------------------------------
# 9. wiki_entry 필수 필드 검증
# ---------------------------------------------------------------------------


class TestRequiredFields:
    def test_all_required_keys_present(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n• 유형 : 보상\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        for entry in result:
            missing = REQUIRED_KEYS - set(entry.keys())
            assert not missing, f"필수 키 누락: {missing}"

    def test_keywords_is_list(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert isinstance(result[0]["keywords"], list)

    def test_raw_thread_is_list(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert isinstance(result[0]["raw_thread"], list)

    def test_confidence_is_valid_value(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages)
        assert result[0]["confidence"] in {"high", "medium", "low"}


# ---------------------------------------------------------------------------
# 10. use_llm=False (기본값) 테스트
# ---------------------------------------------------------------------------


class TestUseLlmFalse:
    def test_rule_based_extraction_without_llm(self):
        """LLM 없이 규칙 기반으로 추출이 가능해야 한다."""
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n• 유형 : 보상\n질문입니다.",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변입니다.",
                type=MessageType.MESSAGE,
            ),
        ]
        # use_llm=False가 기본값이므로 api_key 없이 호출 가능해야 함
        result = extract_knowledge(messages, use_llm=False)
        assert len(result) == 1

    def test_returns_list(self):
        messages = [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n질문",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변",
                type=MessageType.MESSAGE,
            ),
        ]
        result = extract_knowledge(messages, use_llm=False)
        assert isinstance(result, list)

    def test_empty_messages_returns_empty_list(self):
        result = extract_knowledge([], use_llm=False)
        assert result == []


# ---------------------------------------------------------------------------
# 11. id 순번 포맷
# ---------------------------------------------------------------------------


class TestIdFormat:
    def _make_two_thread_messages(self):
        return [
            ChatMessage(
                date="2025-12-03",
                time="17:00",
                user="A/B/C",
                content="#궁금증\n• 유형 : 보상\n질문1",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="17:05",
                user="D/E/F",
                content="답변1",
                type=MessageType.MESSAGE,
            ),
            # 30분 이상 gap → 새 스레드
            ChatMessage(
                date="2025-12-03",
                time="18:00",
                user="G/H/I",
                content="#궁금증\n• 유형 : 약관\n질문2",
                type=MessageType.MESSAGE,
            ),
            ChatMessage(
                date="2025-12-03",
                time="18:05",
                user="D/E/F",
                content="답변2",
                type=MessageType.MESSAGE,
            ),
        ]

    def test_first_entry_id(self):
        result = extract_knowledge(self._make_two_thread_messages())
        assert result[0]["id"] == "kakao-001"

    def test_second_entry_id(self):
        result = extract_knowledge(self._make_two_thread_messages())
        assert result[1]["id"] == "kakao-002"

    def test_id_zero_padded_three_digits(self):
        """id는 3자리 zero-padding이어야 한다."""
        result = extract_knowledge(self._make_two_thread_messages())
        for entry in result:
            parts = entry["id"].split("-")
            assert len(parts) == 2
            assert parts[0] == "kakao"
            assert parts[1].isdigit()
            assert len(parts[1]) == 3


# ---------------------------------------------------------------------------
# 12. 스레드 분리: #궁금증 태그 기반 (같은 시간대)
# ---------------------------------------------------------------------------


class TestThreadSplitByQuestionTag:
    """#궁금증 태그 기반 스레드 분리: 시간 gap 없어도 태그마다 새 스레드"""

    def test_two_question_tags_same_time_creates_two_entries(self):
        """같은 시간대에 #궁금증이 2번 나오면 2개 항목 생성"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:03", user="C/KB/대전",
                        content="#궁금증\n• 유형 : 약관\n질문2입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:05", user="B/프라임/부산",
                        content="답변2입니다", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 2

    def test_first_tag_gets_correct_category(self):
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:03", user="C/KB/대전",
                        content="#궁금증\n• 유형 : 고지의무\n질문2", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:05", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert result[0]["category"] == "보상"

    def test_second_tag_gets_correct_category(self):
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:03", user="C/KB/대전",
                        content="#궁금증\n• 유형 : 고지의무\n질문2", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:05", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert result[1]["category"] == "고지의무"


# ---------------------------------------------------------------------------
# 13. 스레드 분리: 질문 패턴 감지
# ---------------------------------------------------------------------------


class TestThreadSplitByQuestionPattern:
    """질문 패턴 감지 기반 스레드 분리"""

    def test_질문_드립니다_creates_new_thread(self):
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:04", user="C/KB/대전",
                        content="질문 드립니다. 실비 청구 관련해서요.", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:06", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 2

    def test_문의_드립니다_creates_new_thread(self):
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:04", user="C/KB/대전",
                        content="문의 드립니다. 고지의무 관련입니다.", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:06", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 2

    def test_궁금합니다_creates_new_thread(self):
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n• 유형 : 보상\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:04", user="C/KB/대전",
                        content="약관 해석이 궁금합니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:06", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 2


# ---------------------------------------------------------------------------
# 14. 스레드 분리: 15분 gap
# ---------------------------------------------------------------------------


class TestThreadSplitBy15MinGap:
    """15분 gap 기반 스레드 분리 (30분→15분 축소)"""

    def test_16min_gap_creates_new_thread(self):
        """16분 gap → 새 스레드"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:05", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:21", user="C/KB/대전",
                        content="#궁금증\n질문2", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:25", user="B/프라임/부산",
                        content="답변2", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 2

    def test_14min_gap_same_thread(self):
        """14분 gap → 같은 스레드 (15분 미만)"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:05", user="B/프라임/부산",
                        content="답변1", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:19", user="B/프라임/부산",
                        content="추가 답변", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 1


# ---------------------------------------------------------------------------
# 15. 노이즈 필터링
# ---------------------------------------------------------------------------


class TestNoiseFiltering:
    """노이즈 메시지 필터링"""

    def test_simple_greeting_excluded_from_raw_thread(self):
        """단순 인사 메시지는 raw_thread에서 제외"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:01", user="X/GA/경기",
                        content="안녕하세요", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변입니다", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        assert len(result) == 1
        raw_texts = " ".join(result[0]["raw_thread"])
        # 단순 인사만 있는 메시지는 제외되어야 함
        assert "[X/GA/경기]" not in raw_texts

    def test_reaction_excluded_from_raw_thread(self):
        """리액션 메시지(ㅋㅋ, ㅎㅎ)는 raw_thread에서 제외"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:01", user="X/GA/경기",
                        content="ㅋㅋㅋ", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변입니다", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        raw_texts = " ".join(result[0]["raw_thread"])
        assert "ㅋㅋㅋ" not in raw_texts

    def test_감사합니다_only_excluded(self):
        """감사합니다만 있는 메시지는 제외"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:03", user="A/인카/서울",
                        content="감사합니다", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        # raw_thread에 감사합니다만 있는 줄이 없어야 함
        for line in result[0]["raw_thread"]:
            if "A/인카/서울" in line:
                # 질문 메시지는 있어야 하지만, 감사합니다만 있는 건 제외
                assert "감사합니다" not in line or "질문" in line

    def test_substantive_greeting_with_question_not_excluded(self):
        """안녕하세요 + 질문이 같이 있는 메시지는 제거하지 않음"""
        messages = [
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="#궁금증\n질문입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="안녕하세요 실비 관련 답변드립니다", type=MessageType.MESSAGE),
        ]
        result = extract_knowledge(messages)
        raw_texts = " ".join(result[0]["raw_thread"])
        assert "실비 관련 답변" in raw_texts


# ---------------------------------------------------------------------------
# 16. LLM 정제 결과 JSON 파싱 (mock)
# ---------------------------------------------------------------------------


class TestLlmPromptJsonParsing:
    """LLM 정제 결과 JSON 파싱 테스트 (mock)"""

    def test_valid_json_response_parsed(self):
        """유효한 JSON 응답이 올바르게 파싱됨"""
        from unittest.mock import MagicMock
        from kakao_knowledge.knowledge_extractor import _analyze_thread_with_llm, Thread

        thread = Thread(messages=[
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="질문입니다", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="답변입니다", type=MessageType.MESSAGE),
        ], start_time="2025-12-03 17:00", has_question_tag=True)

        mock_response = MagicMock()
        mock_response.content = [MagicMock()]
        mock_response.content[0].text = '{"title": "테스트 제목", "category": "보상", "question": "질문", "answer": "답변", "keywords": ["보상", "실비"], "confidence": "high", "is_noise": false}'

        mock_client = MagicMock()
        mock_client.messages.create.return_value = mock_response

        result = _analyze_thread_with_llm(thread, mock_client, 1, "test_chat")
        assert result is not None
        assert result["title"] == "테스트 제목"
        assert result["category"] == "보상"
        assert result["confidence"] == "high"

    def test_noise_response_detected(self):
        """is_noise: true 응답이 올바르게 감지됨"""
        from unittest.mock import MagicMock
        from kakao_knowledge.knowledge_extractor import _analyze_thread_with_llm, Thread

        thread = Thread(messages=[
            ChatMessage(date="2025-12-03", time="17:00", user="A/인카/서울",
                        content="안녕하세요~", type=MessageType.MESSAGE),
            ChatMessage(date="2025-12-03", time="17:02", user="B/프라임/부산",
                        content="안녕하세요!", type=MessageType.MESSAGE),
        ], start_time="2025-12-03 17:00", has_question_tag=False)

        mock_response = MagicMock()
        mock_response.content = [MagicMock()]
        mock_response.content[0].text = '{"is_noise": true, "reason": "단순 인사"}'

        mock_client = MagicMock()
        mock_client.messages.create.return_value = mock_response

        result = _analyze_thread_with_llm(thread, mock_client, 1, "test_chat")
        assert result is None  # noise는 None 반환
