"""
TDD RED 단계: test_insurance_crawler.py

보험사 공개 데이터 크롤러 InsuranceCrawler 클래스에 대한 테스트 스위트.
(insurance_crawler.py는 아직 구현되지 않음 - TDD RED 단계)

주의:
- 모든 테스트는 로컬 HTML 문자열로 수행. 외부 네트워크 호출 없음.
- 실제 보험사 사이트 크롤링은 robots.txt를 반드시 확인하고 준수해야 합니다.
- 합법적 공개 데이터만 대상으로 합니다.
"""

import sys
from pathlib import Path
from types import ModuleType
from typing import Optional
from unittest.mock import MagicMock, patch

import pytest

# scripts 디렉토리를 path에 추가
sys.path.insert(0, str(Path(__file__).parent.parent))

# insurance_crawler가 아직 구현되지 않을 수 있으므로 안전하게 import
_insurance_crawler: Optional[ModuleType] = None
try:
    import insurance_crawler as _insurance_crawler  # pyright: ignore[reportMissingImports]
except ModuleNotFoundError:
    pass

_MISSING = _insurance_crawler is None
_skip_if_missing = pytest.mark.skipif(
    _MISSING,
    reason="insurance_crawler.py 미구현 (TDD RED 단계 - 토르가 구현 예정)",
)

# ────────────────────────────────────────────────────────
# 공통 HTML 픽스처
# ────────────────────────────────────────────────────────

HTML_PRODUCTS = """
<html>
<body>
  <div class="product">
    <span class="name">화재보험</span>
    <span class="price">50000</span>
  </div>
  <div class="product">
    <span class="name">자동차보험</span>
    <span class="price">100000</span>
  </div>
  <div class="product">
    <span class="name">생명보험</span>
    <span class="price">30000</span>
  </div>
</body>
</html>
"""

HTML_TABLE = """
<html>
<body>
  <table>
    <tr><th>보험명</th><th>보험료</th><th>가입기간</th></tr>
    <tr><td>화재보험</td><td>50000</td><td>1년</td></tr>
    <tr><td>자동차보험</td><td>100000</td><td>1년</td></tr>
  </table>
</body>
</html>
"""

HTML_TABLE_NO_HEADER = """
<html>
<body>
  <table>
    <tr><td>화재보험</td><td>50000</td></tr>
    <tr><td>자동차보험</td><td>100000</td></tr>
  </table>
</body>
</html>
"""

HTML_EMPTY_TABLE = """
<html>
<body>
  <table></table>
</body>
</html>
"""

HTML_WITH_SCRIPT = """
<html>
<body>
  <script>alert('xss');</script>
  <p>보험 공시 정보</p>
  <div class="product"><span class="name">화재보험</span></div>
</body>
</html>
"""

HTML_SIMPLE = "<html><body><p>보험 정보</p></body></html>"

HTML_EMPTY = ""


# ────────────────────────────────────────────────────────
# TestInsuranceCrawlerInit
# ────────────────────────────────────────────────────────


class TestInsuranceCrawlerInit:
    """InsuranceCrawler 초기화 테스트."""

    @_skip_if_missing
    def test_default_init_adaptive_true(self) -> None:
        """기본 생성 시 adaptive=True로 초기화되어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        assert crawler.adaptive is True

    @_skip_if_missing
    def test_init_with_proxy_list(self) -> None:
        """프록시 리스트 전달 시 ProxyRotator 인스턴스가 생성되어야 한다."""
        assert _insurance_crawler is not None
        from crawl_utils import ProxyRotator

        proxy_list = ["http://proxy1:8080", "http://proxy2:8080"]
        crawler = _insurance_crawler.InsuranceCrawler(proxy_list=proxy_list)
        assert crawler.proxy_rotator is not None
        assert isinstance(crawler.proxy_rotator, ProxyRotator)

    @_skip_if_missing
    def test_init_without_proxy(self) -> None:
        """프록시 없으면 proxy_rotator는 None이어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        assert crawler.proxy_rotator is None

    @_skip_if_missing
    def test_init_adaptive_false(self) -> None:
        """adaptive=False 전달 시 adaptive 속성이 False여야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        assert crawler.adaptive is False


# ────────────────────────────────────────────────────────
# TestInsuranceCrawlerParse
# ────────────────────────────────────────────────────────


class TestInsuranceCrawlerParse:
    """InsuranceCrawler.parse() 메서드 테스트."""

    @_skip_if_missing
    def test_parse_returns_selector(self) -> None:
        """parse()가 Selector 인스턴스를 반환해야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.parse(HTML_SIMPLE)
        assert isinstance(result, Selector)

    @_skip_if_missing
    def test_parse_with_url(self) -> None:
        """url 인자가 Selector에 전달되어야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler()
        url = "https://example-insurance.co.kr"
        result = crawler.parse(HTML_SIMPLE, url=url)
        assert isinstance(result, Selector)
        # url 속성 또는 내부 저장 확인
        assert result.url == url

    @_skip_if_missing
    def test_parse_adaptive_mode(self) -> None:
        """adaptive=True인 crawler로 parse()하면 Selector가 adaptive 설정으로 생성되어야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler(adaptive=True)
        result = crawler.parse(HTML_SIMPLE)
        assert isinstance(result, Selector)
        # Selector 내부 adaptive 플래그 확인 (_Selector__adaptive_enabled)
        assert result._Selector__adaptive_enabled is True  # pyright: ignore[reportAttributeAccessIssue]

    @_skip_if_missing
    def test_parse_non_adaptive_mode(self) -> None:
        """adaptive=False인 crawler로 parse()하면 Selector의 adaptive가 False여야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        result = crawler.parse(HTML_SIMPLE)
        assert isinstance(result, Selector)
        assert result._Selector__adaptive_enabled is False  # pyright: ignore[reportAttributeAccessIssue]


# ────────────────────────────────────────────────────────
# TestExtractWithSelector
# ────────────────────────────────────────────────────────


class TestExtractWithSelector:
    """InsuranceCrawler.extract_with_selector() 메서드 테스트."""

    @_skip_if_missing
    def test_extract_basic_fields(self) -> None:
        """CSS 셀렉터로 name, price 필드를 올바르게 추출해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            fields={"name": ".name", "price": ".price"},
        )
        assert isinstance(result, list)
        assert len(result) == 3
        assert result[0]["name"] == "화재보험"
        assert result[0]["price"] == "50000"
        assert result[1]["name"] == "자동차보험"
        assert result[2]["name"] == "생명보험"

    @_skip_if_missing
    def test_extract_empty_selector(self) -> None:
        """매칭 없는 CSS 셀렉터이면 빈 리스트를 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".nonexistent",
            fields={"name": ".name"},
        )
        assert result == []

    @_skip_if_missing
    def test_extract_no_fields(self) -> None:
        """fields=None이면 컨테이너의 텍스트를 'text' 키로 추출해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            fields=None,
        )
        assert isinstance(result, list)
        assert len(result) == 3
        # 각 항목에 text 키가 있어야 함
        for item in result:
            assert "text" in item

    @_skip_if_missing
    def test_extract_with_identifier(self) -> None:
        """identifier 전달 시 예외 없이 실행되고 결과를 반환해야 한다 (auto_save 동작)."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            identifier="test_product_list",
            fields={"name": ".name"},
        )
        assert isinstance(result, list)
        assert len(result) == 3

    @_skip_if_missing
    def test_extract_partial_fields(self) -> None:
        """일부 필드 서브셀렉터가 없으면 해당 값은 None으로 채워야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            fields={"name": ".name", "category": ".nonexistent-category"},
        )
        assert len(result) == 3
        assert result[0]["name"] == "화재보험"
        # 존재하지 않는 서브셀렉터는 None
        assert result[0]["category"] is None

    @_skip_if_missing
    def test_extract_returns_list_of_dicts(self) -> None:
        """반환값은 list[dict] 타입이어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            fields={"name": ".name"},
        )
        assert isinstance(result, list)
        for item in result:
            assert isinstance(item, dict)


# ────────────────────────────────────────────────────────
# TestExtractSimilar
# ────────────────────────────────────────────────────────


class TestExtractSimilar:
    """InsuranceCrawler.extract_similar() 메서드 테스트."""

    @_skip_if_missing
    def test_find_similar_basic(self) -> None:
        """유사 구조 요소들을 모두 찾아 리스트를 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_similar(
            page,
            reference_selector=".product",
        )
        assert isinstance(result, list)
        # 기준 + 유사 요소 포함 (1개 이상)
        assert len(result) >= 1

    @_skip_if_missing
    def test_find_similar_with_fields(self) -> None:
        """필드 매핑 적용 시 각 항목에 필드 키가 존재해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_similar(
            page,
            reference_selector=".product",
            fields={"name": ".name", "price": ".price"},
        )
        assert isinstance(result, list)
        for item in result:
            assert "name" in item
            assert "price" in item

    @_skip_if_missing
    def test_find_similar_no_match(self) -> None:
        """기준 셀렉터가 매칭되지 않으면 빈 리스트를 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        result = crawler.extract_similar(
            page,
            reference_selector=".nonexistent-element",
        )
        assert result == []

    @_skip_if_missing
    def test_find_similar_threshold(self) -> None:
        """임계값 인자가 정상적으로 전달되어 실행되어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_PRODUCTS)
        # threshold 0.0으로 가장 느슨하게 설정
        result_loose = crawler.extract_similar(
            page,
            reference_selector=".product",
            threshold=0.0,
        )
        assert isinstance(result_loose, list)
        # threshold 0.9으로 엄격하게 설정
        result_strict = crawler.extract_similar(
            page,
            reference_selector=".product",
            threshold=0.9,
        )
        assert isinstance(result_strict, list)


# ────────────────────────────────────────────────────────
# TestExtractTable
# ────────────────────────────────────────────────────────


class TestExtractTable:
    """InsuranceCrawler.extract_table() 메서드 테스트."""

    @_skip_if_missing
    def test_extract_table_basic(self) -> None:
        """기본 테이블 추출: th 헤더 + td 데이터가 딕셔너리로 변환되어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_TABLE)
        result = crawler.extract_table(page)
        assert isinstance(result, list)
        assert len(result) == 2
        assert result[0]["보험명"] == "화재보험"
        assert result[0]["보험료"] == "50000"
        assert result[0]["가입기간"] == "1년"
        assert result[1]["보험명"] == "자동차보험"

    @_skip_if_missing
    def test_extract_table_no_header(self) -> None:
        """헤더(th) 없으면 col_0, col_1... 형태로 자동 명명해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_TABLE_NO_HEADER)
        result = crawler.extract_table(page)
        assert isinstance(result, list)
        assert len(result) == 2
        assert "col_0" in result[0]
        assert "col_1" in result[0]
        assert result[0]["col_0"] == "화재보험"
        assert result[0]["col_1"] == "50000"

    @_skip_if_missing
    def test_extract_table_empty(self) -> None:
        """빈 테이블이면 빈 리스트를 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_EMPTY_TABLE)
        result = crawler.extract_table(page)
        assert result == []

    @_skip_if_missing
    def test_extract_table_custom_selectors(self) -> None:
        """커스텀 table_selector를 사용하여 특정 테이블을 선택할 수 있어야 한다."""
        assert _insurance_crawler is not None
        html = """<html><body>
        <table id="insurance-table">
          <tr><th>상품명</th><th>월납입료</th></tr>
          <tr><td>종신보험</td><td>200000</td></tr>
        </table>
        </body></html>"""
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(html)
        result = crawler.extract_table(page, table_selector="#insurance-table")
        assert isinstance(result, list)
        assert len(result) == 1
        assert result[0]["상품명"] == "종신보험"
        assert result[0]["월납입료"] == "200000"

    @_skip_if_missing
    def test_extract_table_no_table_returns_empty(self) -> None:
        """테이블이 없는 HTML에서 extract_table() 호출 시 빈 리스트 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = crawler.parse(HTML_SIMPLE)
        result = crawler.extract_table(page)
        assert result == []


# ────────────────────────────────────────────────────────
# TestToLlmInput
# ────────────────────────────────────────────────────────


class TestToLlmInput:
    """InsuranceCrawler.to_llm_input() 메서드 테스트."""

    @_skip_if_missing
    def test_to_llm_input_basic(self) -> None:
        """HTML을 마크다운으로 변환하고 문자열을 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.to_llm_input("<p>보험 공시 정보</p>")
        assert isinstance(result, str)
        assert "보험 공시 정보" in result

    @_skip_if_missing
    def test_to_llm_input_removes_script(self) -> None:
        """script 태그가 제거되어야 한다 (clean_html 파이프라인)."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.to_llm_input(HTML_WITH_SCRIPT)
        assert "alert" not in result
        assert "xss" not in result

    @_skip_if_missing
    def test_to_llm_input_preserves_text(self) -> None:
        """텍스트 내용은 결과에 보존되어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.to_llm_input(HTML_WITH_SCRIPT)
        assert "보험 공시 정보" in result

    @_skip_if_missing
    def test_to_llm_input_empty(self) -> None:
        """빈 HTML 입력 시 빈 문자열 또는 최소한 str을 반환해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.to_llm_input(HTML_EMPTY)
        assert isinstance(result, str)
        # 빈 HTML은 실질적인 내용이 없어야 함
        assert result.strip() == ""

    @_skip_if_missing
    def test_to_llm_input_returns_string(self) -> None:
        """반환값은 반드시 str 타입이어야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler()
        result = crawler.to_llm_input(HTML_SIMPLE)
        assert isinstance(result, str)


# ────────────────────────────────────────────────────────
# TestExtractFields (내부 헬퍼)
# ────────────────────────────────────────────────────────


class TestExtractFields:
    """InsuranceCrawler._extract_fields() 내부 헬퍼 메서드 테스트."""

    @_skip_if_missing
    def test_extract_fields_basic(self) -> None:
        """기본 필드 추출: elements에서 fields 매핑에 따라 데이터를 추출해야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = Selector(HTML_PRODUCTS, adaptive=False)
        elements = page.css(".product")
        result = crawler._extract_fields(elements, {"name": ".name", "price": ".price"})
        assert isinstance(result, list)
        assert len(result) == 3
        assert result[0]["name"] == "화재보험"
        assert result[0]["price"] == "50000"

    @_skip_if_missing
    def test_extract_fields_missing_sub_selector(self) -> None:
        """서브셀렉터가 존재하지 않으면 해당 필드 값은 None이어야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = Selector(HTML_PRODUCTS, adaptive=False)
        elements = page.css(".product")
        result = crawler._extract_fields(elements, {"name": ".name", "discount": ".nonexistent"})
        assert len(result) == 3
        assert result[0]["name"] == "화재보험"
        assert result[0]["discount"] is None

    @_skip_if_missing
    def test_extract_fields_multiple_elements(self) -> None:
        """복수 요소에서 일괄 추출 시 모든 요소가 처리되어야 한다."""
        assert _insurance_crawler is not None
        from scrapling.parser import Selector

        crawler = _insurance_crawler.InsuranceCrawler(adaptive=False)
        page = Selector(HTML_PRODUCTS, adaptive=False)
        elements = page.css(".product")
        result = crawler._extract_fields(elements, {"name": ".name"})
        # 3개 상품 모두 추출
        assert len(result) == 3
        names = [item["name"] for item in result]
        assert "화재보험" in names
        assert "자동차보험" in names
        assert "생명보험" in names


# ────────────────────────────────────────────────────────
# TestSmartMatchingIntegration
# ────────────────────────────────────────────────────────


class TestSmartMatchingIntegration:
    """Smart Matching 통합 테스트 (auto_save + adaptive 재탐색)."""

    @_skip_if_missing
    def test_auto_save_and_adaptive(self) -> None:
        """auto_save로 저장 후 구조 변경된 HTML에서 adaptive로 재탐색해야 한다.

        시나리오:
        1. 원본 HTML로 extract_with_selector() (auto_save=True, identifier 포함)
        2. 구조 변경된 HTML로 adaptive 모드 재탐색
        3. 결과가 리스트 형태로 반환되는지 확인 (adaptive 재탐색 성공 여부는 환경 의존)
        """
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=True)

        # 원본 구조로 저장
        page_original = crawler.parse(HTML_PRODUCTS, url="https://example-insurance.co.kr/products")
        result1 = crawler.extract_with_selector(
            page_original,
            css_selector=".product",
            identifier="insurance_product_adaptive_test",
            fields={"name": ".name", "price": ".price"},
        )
        assert len(result1) == 3

        # 구조가 변경된 HTML (클래스명 변경 시나리오)
        html_changed = """
        <html>
        <body>
          <article class="insurance-item">
            <h3 class="name">화재보험</h3>
            <span class="price">50000</span>
          </article>
        </body>
        </html>
        """
        page_changed = crawler.parse(html_changed, url="https://example-insurance.co.kr/products")
        # adaptive 모드에서 새 구조로 탐색 (결과는 환경에 따라 다를 수 있음)
        result2 = crawler.extract_with_selector(
            page_changed,
            css_selector=".product",
            identifier="insurance_product_adaptive_test",
            fields={"name": ".name", "price": ".price"},
        )
        # 결과가 리스트 형태여야 함 (adaptive 성공 시 요소 반환, 실패 시 빈 리스트)
        assert isinstance(result2, list)

    @_skip_if_missing
    def test_extract_with_selector_adaptive_enabled(self) -> None:
        """adaptive=True 크롤러는 extract_with_selector()에서 adaptive 옵션을 활성화해야 한다."""
        assert _insurance_crawler is not None
        crawler = _insurance_crawler.InsuranceCrawler(adaptive=True)
        page = crawler.parse(HTML_PRODUCTS, url="https://example.com")
        # adaptive 모드에서도 일반 추출이 정상 동작해야 함
        result = crawler.extract_with_selector(
            page,
            css_selector=".product",
            identifier="adaptive_test",
            fields={"name": ".name"},
        )
        assert isinstance(result, list)
        assert len(result) == 3
