#!/usr/bin/env python3
"""utils/session_search.py 테스트 스위트 (TDD — RED → GREEN)"""

from __future__ import annotations

import sys
from pathlib import Path

import pytest

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

from utils.session_search import (
    _format_conversation,
    _resolve_lineage_root,
    _truncate_around_matches,
    sanitize_fts5_query,
    search_sessions,
)
from utils.session_store import SessionStore

# ---------------------------------------------------------------------------
# 픽스처
# ---------------------------------------------------------------------------


@pytest.fixture
def store(tmp_path):
    """임시 DB 경로를 사용하는 SessionStore."""
    db_path = str(tmp_path / "test_search.db")
    s = SessionStore(db_path=db_path)
    yield s
    s.close()


@pytest.fixture
def populated_store(store):
    """검색 테스트용 세션/메시지가 있는 store."""
    store.create_session("sess-alpha", source="api")
    store.append_message("sess-alpha", role="user", content="hello world FTS5 test")
    store.append_message("sess-alpha", role="assistant", content="response to hello")

    store.create_session("sess-beta", source="api")
    store.append_message("sess-beta", role="user", content="python programming rocks")

    store.create_session("sess-gamma", source="api")
    store.append_message("sess-gamma", role="user", content="completely unrelated content")
    return store


# ---------------------------------------------------------------------------
# 1. sanitize_fts5_query
# ---------------------------------------------------------------------------


class TestSanitizeFts5Query:
    """FTS5 쿼리 특수문자 처리"""

    def test_plain_word_unchanged(self):
        """일반 단어는 변경 없이 반환된다."""
        assert sanitize_fts5_query("hello") == "hello"

    def test_removes_parentheses(self):
        """괄호가 제거된다."""
        result = sanitize_fts5_query("(hello world)")
        assert "(" not in result
        assert ")" not in result

    def test_removes_plus_operator(self):
        """+ 연산자가 제거된다."""
        result = sanitize_fts5_query("hello+world")
        assert "+" not in result

    def test_removes_star_operator(self):
        """* 연산자가 제거된다."""
        result = sanitize_fts5_query("hello*")
        assert "*" not in result

    def test_quoted_phrase_preserved(self):
        """인용구("...")는 보호된다."""
        result = sanitize_fts5_query('"hello world"')
        assert '"hello world"' in result

    def test_removes_and_operator(self):
        """AND 연산자가 제거된다."""
        result = sanitize_fts5_query("hello AND world")
        assert "AND" not in result
        assert "hello" in result
        assert "world" in result

    def test_removes_or_operator(self):
        """OR 연산자가 제거된다."""
        result = sanitize_fts5_query("hello OR world")
        assert "OR" not in result

    def test_removes_not_operator(self):
        """NOT 연산자가 제거된다."""
        result = sanitize_fts5_query("hello NOT world")
        assert "NOT" not in result

    def test_android_preserved(self):
        """ANDROID 같은 단어 안의 AND는 보호된다."""
        result = sanitize_fts5_query("ANDROID")
        assert "ANDROID" in result

    def test_hyphenated_word_quoted(self):
        """하이픈 단어(chat-send)가 인용 처리된다."""
        result = sanitize_fts5_query("chat-send")
        assert '"chat-send"' in result

    def test_empty_query_returns_empty(self):
        """빈 쿼리는 빈 문자열 반환."""
        assert sanitize_fts5_query("") == ""

    def test_whitespace_normalized(self):
        """연속 공백이 정규화된다."""
        result = sanitize_fts5_query("hello   world")
        assert "  " not in result.strip()


# ---------------------------------------------------------------------------
# 2. search_sessions
# ---------------------------------------------------------------------------


class TestSearchSessions:
    """FTS5 세션 검색 동작"""

    def test_basic_search_returns_results(self, populated_store):
        """기본 검색이 결과를 반환한다."""
        result = search_sessions("hello", populated_store)
        assert isinstance(result, dict)
        assert "results" in result

    def test_matching_session_found(self, populated_store):
        """매칭 세션이 결과에 포함된다."""
        result = search_sessions("hello", populated_store)
        session_ids = [r["session_id"] for r in result["results"]]
        assert "sess-alpha" in session_ids

    def test_limit_respected(self, populated_store):
        """limit 파라미터가 결과 수를 제한한다."""
        result = search_sessions("hello", populated_store, limit=1)
        assert len(result["results"]) <= 1

    def test_empty_query_returns_empty_results(self, populated_store):
        """빈 쿼리는 빈 결과를 반환한다."""
        result = search_sessions("", populated_store)
        assert result["results"] == []

    def test_no_match_returns_empty(self, populated_store):
        """매칭 없는 쿼리는 빈 결과."""
        result = search_sessions("xyzzy_no_match_ever", populated_store)
        assert result["results"] == []

    def test_role_filter_user_only(self, populated_store):
        """role_filter=['user']이면 user 메시지만 검색한다."""
        result = search_sessions("response", populated_store, role_filter=["user"])
        # "response to hello"는 assistant 메시지이므로 user 필터 시 제외
        session_ids = [r["session_id"] for r in result["results"]]
        # sess-alpha의 user 메시지에는 "response"가 없음
        assert "sess-alpha" not in session_ids or len(result["results"]) == 0

    def test_current_session_excluded(self, populated_store):
        """current_session_id가 설정되면 해당 세션이 결과에서 제외된다."""
        result = search_sessions("hello", populated_store, current_session_id="sess-alpha")
        session_ids = [r["session_id"] for r in result["results"]]
        assert "sess-alpha" not in session_ids

    def test_result_contains_summary(self, populated_store):
        """결과 항목에 summary 필드가 있다."""
        result = search_sessions("hello", populated_store)
        if result["results"]:
            assert "summary" in result["results"][0]

    def test_result_contains_session_id(self, populated_store):
        """결과 항목에 session_id 필드가 있다."""
        result = search_sessions("hello", populated_store)
        if result["results"]:
            assert "session_id" in result["results"][0]


# ---------------------------------------------------------------------------
# 3. _resolve_lineage_root
# ---------------------------------------------------------------------------


class TestResolveLineageRoot:
    """위임 체인 루트 탐색"""

    def test_single_session_returns_self(self, store):
        """parent_session_id가 없으면 자신이 루트."""
        store.create_session("root-only", source="api")
        assert _resolve_lineage_root("root-only", store) == "root-only"

    def test_child_returns_root(self, store):
        """부모 체인을 타고 루트를 찾는다."""
        store.create_session("root-1", source="api")
        store.create_session("child-1", source="api", parent_session_id="root-1")
        assert _resolve_lineage_root("child-1", store) == "root-1"

    def test_deep_chain_returns_root(self, store):
        """3단계 체인에서도 루트를 찾는다."""
        store.create_session("r", source="api")
        store.create_session("c1", source="api", parent_session_id="r")
        store.create_session("c2", source="api", parent_session_id="c1")
        assert _resolve_lineage_root("c2", store) == "r"

    def test_unknown_session_returns_self(self, store):
        """존재하지 않는 session_id는 자신을 반환한다 (안전 폴백)."""
        assert _resolve_lineage_root("nonexistent", store) == "nonexistent"

    def test_cycle_guard(self, store):
        """순환 참조가 있어도 무한루프 없이 종료된다."""
        # 순환을 강제로 DB에 주입
        import sqlite3

        store.create_session("cycle-a", source="api")
        store.create_session("cycle-b", source="api", parent_session_id="cycle-a")
        conn = sqlite3.connect(store.db_path)
        conn.execute("UPDATE sessions SET parent_session_id='cycle-b' WHERE session_id='cycle-a'")
        conn.commit()
        conn.close()
        # 무한루프 없이 어떤 값이든 반환해야 한다
        result = _resolve_lineage_root("cycle-a", store)
        assert isinstance(result, str)


# ---------------------------------------------------------------------------
# 4. _truncate_around_matches
# ---------------------------------------------------------------------------


class TestTruncateAroundMatches:
    """쿼리 매칭 위치 중심 텍스트 창 추출"""

    def test_short_text_returned_as_is(self):
        """max_chars보다 짧은 텍스트는 그대로 반환된다."""
        text = "hello world"
        result = _truncate_around_matches(text, "hello", max_chars=1000)
        assert "hello" in result

    def test_long_text_truncated(self):
        """max_chars보다 긴 텍스트는 잘린다."""
        text = "x" * 200_000
        result = _truncate_around_matches(text, "xxx", max_chars=100)
        assert len(result) <= 200  # 여유 마진 허용

    def test_match_position_included(self):
        """매칭 위치 주변 텍스트가 포함된다."""
        prefix = "a" * 50_000
        text = prefix + "TARGET" + "b" * 50_000
        result = _truncate_around_matches(text, "TARGET", max_chars=500)
        assert "TARGET" in result

    def test_empty_query_returns_head(self):
        """빈 쿼리는 텍스트 앞부분을 반환한다."""
        text = "hello world " * 10000
        result = _truncate_around_matches(text, "", max_chars=100)
        assert len(result) <= 200


# ---------------------------------------------------------------------------
# 5. _format_conversation
# ---------------------------------------------------------------------------


class TestFormatConversation:
    """메시지 직렬화"""

    def test_empty_messages_returns_string(self):
        """빈 메시지 목록은 문자열을 반환한다."""
        result = _format_conversation([])
        assert isinstance(result, str)

    def test_message_role_included(self):
        """메시지 role이 포맷에 포함된다."""
        msgs = [{"role": "user", "content": "hello", "created_at": "2024-01-01"}]
        result = _format_conversation(msgs)
        assert "user" in result

    def test_message_content_included(self):
        """메시지 content가 포맷에 포함된다."""
        msgs = [{"role": "user", "content": "unique_content_xyz"}]
        result = _format_conversation(msgs)
        assert "unique_content_xyz" in result
