"""
test_schema_contract.py - schema_contract verifier 단위 테스트 (헤임달 작성)

테스트 항목:
1. 정상 케이스 - 모든 SC 항목 PASS
2. SC-1: models.py 누락
3. SC-2: tests/test_contract.py 누락
4. SC-3: shared/schemas/{name}.schema.json 누락
5. SC-4/SC-5: sample 데이터가 JSON Schema와 불일치
6. SC-6: Pydantic 모델 필드와 JSON Schema 필드 불일치 (mock 사용)
7. workers/ 디렉토리 자체 없음 → SKIP
8. jsonschema 라이브러리 부재 → SC-4/SC-5 SKIP + WARN

구현 참고 사항:
- sample 파일 경로: schemas_dir/{name}.sample.normal.json, {name}.sample.edge.json
- SC-1 누락 시 Worker 자체가 감지되지 않으므로 → SKIP
  (schema_contract는 models.py 존재 여부로 Worker 디렉토리를 감지)
  따라서 SC-1 FAIL은 Worker를 먼저 정상 감지한 후 SC-1 로직이 실행될 때 발생함.
  실제 구현에서는 _find_worker_dirs()가 models.py 기준으로 탐색하므로
  models.py 없으면 Worker 자체를 못 찾아 SKIP 반환.
  → SC-1 테스트는 "models.py 없는 Worker" 대신 "SC-1 FAIL 메시지 직접 발생" 시나리오로 구성.
- jsonschema 미설치 시: 전체 SKIP이 아닌 WARN + SC-4/SC-5 개별 SKIP
"""

import json
import sys
from pathlib import Path
from unittest import mock

import pytest

# qc 디렉토리를 sys.path에 추가하여 verifiers 모듈 임포트 가능하도록 설정
_QC_DIR = Path(__file__).parent.parent
if str(_QC_DIR) not in sys.path:
    sys.path.insert(0, str(_QC_DIR))

# verifiers 디렉토리도 경로에 추가
_VERIFIERS_DIR = _QC_DIR / "verifiers"
if str(_VERIFIERS_DIR) not in sys.path:
    sys.path.insert(0, str(_VERIFIERS_DIR))

# schema_contract 모듈 임포트 시도 (아직 구현 안 됐을 수 있음)
try:
    from verifiers import schema_contract
    _MODULE_AVAILABLE = True
except ImportError:
    try:
        import schema_contract  # type: ignore
        _MODULE_AVAILABLE = True
    except ImportError:
        _MODULE_AVAILABLE = False

# 모듈이 없으면 전체 테스트를 건너뜀
pytestmark = pytest.mark.skipif(
    not _MODULE_AVAILABLE,
    reason="schema_contract 모듈이 아직 구현되지 않음 — 구현 후 테스트 활성화",
)


# ---------------------------------------------------------------------------
# 상수
# ---------------------------------------------------------------------------

_VALID_SCHEMA = {
    "type": "object",
    "required": ["keyword", "keyword_count"],
    "properties": {
        "keyword": {"type": "string"},
        "keyword_count": {"type": "integer"},
    },
    "additionalProperties": False,
}

_VALID_NORMAL_SAMPLE = {"keyword": "test", "keyword_count": 42}
_VALID_EDGE_SAMPLE   = {"keyword": "edge", "keyword_count": 0}

_MODELS_PY_CONTENT = """\
from pydantic import BaseModel

class KeywordResult(BaseModel):
    keyword: str
    keyword_count: int
"""

_TEST_CONTRACT_CONTENT = """\
def test_keyword_result_schema():
    pass
"""


# ---------------------------------------------------------------------------
# 헬퍼: 완전한 Worker 디렉토리 구조 생성
#
# 디렉토리 구조:
#   workers/
#     {worker_name}/
#       models.py                          ← SC-1 대상
#       tests/
#         test_contract.py               ← SC-2 대상
#   schemas/                             ← schemas_dir
#     {worker_name}.schema.json          ← SC-3 대상
#     {worker_name}.sample.normal.json   ← SC-4 대상
#     {worker_name}.sample.edge.json     ← SC-5 대상
# ---------------------------------------------------------------------------

def _make_worker_dir(
    base: Path,
    worker_name: str = "keyword_worker",
    *,
    include_models: bool = True,
    include_test_contract: bool = True,
    include_schema: bool = True,
    include_normal_sample: bool = True,
    include_edge_sample: bool = True,
    normal_sample: dict | None = None,
    edge_sample: dict | None = None,
    schema_content: dict | None = None,
    models_content: str | None = None,
) -> tuple[Path, Path]:
    """
    tmp_path 아래에 테스트용 Worker 구조를 생성하고
    (workers_base_dir, schemas_dir) 경로를 반환합니다.
    """
    workers_base = base / "workers"
    worker_dir   = workers_base / worker_name
    tests_dir    = worker_dir / "tests"
    schemas_dir  = base / "schemas"

    worker_dir.mkdir(parents=True, exist_ok=True)
    tests_dir.mkdir(parents=True, exist_ok=True)
    schemas_dir.mkdir(parents=True, exist_ok=True)

    if include_models:
        content = models_content if models_content is not None else _MODELS_PY_CONTENT
        (worker_dir / "models.py").write_text(content, encoding="utf-8")

    if include_test_contract:
        (tests_dir / "test_contract.py").write_text(_TEST_CONTRACT_CONTENT, encoding="utf-8")

    schema = schema_content if schema_content is not None else _VALID_SCHEMA
    if include_schema:
        (schemas_dir / f"{worker_name}.schema.json").write_text(
            json.dumps(schema), encoding="utf-8"
        )

    if include_normal_sample:
        ns = normal_sample if normal_sample is not None else _VALID_NORMAL_SAMPLE
        (schemas_dir / f"{worker_name}.sample.normal.json").write_text(
            json.dumps(ns), encoding="utf-8"
        )

    if include_edge_sample:
        es = edge_sample if edge_sample is not None else _VALID_EDGE_SAMPLE
        (schemas_dir / f"{worker_name}.sample.edge.json").write_text(
            json.dumps(es), encoding="utf-8"
        )

    return workers_base, schemas_dir


# ---------------------------------------------------------------------------
# 1. 정상 케이스
# ---------------------------------------------------------------------------

class TestSchemaContractNormalCase:
    """모든 SC 항목이 충족되면 PASS를 반환해야 한다."""

    def test_sc_all_pass_status(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "PASS", (
            f"정상 환경에서 PASS 기대, got: {result}"
        )

    def test_sc_result_has_required_keys(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert "status" in result, "결과에 'status' 키가 없음"
        assert "details" in result, "결과에 'details' 키가 없음"
        assert isinstance(result["details"], list), "'details'는 list여야 함"

    def test_sc_pass_details_not_empty(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert len(result["details"]) > 0, "PASS여도 details에 최소 1개 항목 필요"

    def test_sc_pass_details_contain_pass_keyword(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "PASS" in details_str or "OK" in details_str, (
            f"PASS 상태인데 details에 PASS/OK 문자열 없음: {details_str}"
        )

    def test_sc_details_contain_sc1_pass(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-1 PASS" in details_str, f"정상 케이스에 SC-1 PASS 없음: {details_str}"

    def test_sc_details_contain_sc2_pass(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-2 PASS" in details_str, f"정상 케이스에 SC-2 PASS 없음: {details_str}"

    def test_sc_details_contain_sc3_pass(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-normal",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-3 PASS" in details_str, f"정상 케이스에 SC-3 PASS 없음: {details_str}"


# ---------------------------------------------------------------------------
# 2. SC-1: models.py 누락
#
# 주의: schema_contract의 Worker 감지는 models.py 유무를 기준으로 하므로
#       models.py가 없으면 Worker 자체가 감지되지 않아 SKIP이 반환된다.
#       SC-1 FAIL은 _verify_worker() 내부에서 발생하는데,
#       _find_worker_dirs()가 models.py 기준으로 탐색하기 때문에
#       실제로 SC-1 FAIL을 유발하려면 _find_worker_dirs를 mock해서
#       models.py 없는 Worker를 강제로 등록시켜야 한다.
# ---------------------------------------------------------------------------

class TestSC1ModelsMissing:
    """Worker 디렉토리에 models.py가 없으면 FAIL + SC-1 메시지."""

    def _make_worker_without_models(self, base: Path, worker_name: str = "keyword_worker"):
        """models.py 없는 Worker 디렉토리를 생성하고, workers_base와 schemas_dir 반환."""
        workers_base = base / "workers"
        worker_dir   = workers_base / worker_name
        tests_dir    = worker_dir / "tests"
        schemas_dir  = base / "schemas"

        worker_dir.mkdir(parents=True, exist_ok=True)
        tests_dir.mkdir(parents=True, exist_ok=True)
        schemas_dir.mkdir(parents=True, exist_ok=True)

        # models.py 없음 (SC-1 위반)
        (tests_dir / "test_contract.py").write_text(_TEST_CONTRACT_CONTENT, encoding="utf-8")
        (schemas_dir / f"{worker_name}.schema.json").write_text(
            json.dumps(_VALID_SCHEMA), encoding="utf-8"
        )
        (schemas_dir / f"{worker_name}.sample.normal.json").write_text(
            json.dumps(_VALID_NORMAL_SAMPLE), encoding="utf-8"
        )
        (schemas_dir / f"{worker_name}.sample.edge.json").write_text(
            json.dumps(_VALID_EDGE_SAMPLE), encoding="utf-8"
        )
        return workers_base, schemas_dir, worker_dir

    def test_sc1_status_fail_via_mock(self, tmp_path):
        """_find_worker_dirs를 mock하여 SC-1 FAIL 시나리오 강제 구성."""
        workers_base, schemas_dir, worker_dir = self._make_worker_without_models(tmp_path)

        # _find_worker_dirs가 models.py 없는 Worker 디렉토리를 반환하도록 mock
        fake_workers = [{"dir": str(worker_dir), "name": "keyword_worker"}]
        with mock.patch.object(schema_contract, "_find_worker_dirs", return_value=fake_workers):
            result = schema_contract.verify(
                task_id="task-test-sc1",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

        assert result["status"] == "FAIL", (
            f"models.py 누락 시 FAIL 기대, got: {result}"
        )

    def test_sc1_details_contain_sc1_via_mock(self, tmp_path):
        workers_base, schemas_dir, worker_dir = self._make_worker_without_models(tmp_path)
        fake_workers = [{"dir": str(worker_dir), "name": "keyword_worker"}]
        with mock.patch.object(schema_contract, "_find_worker_dirs", return_value=fake_workers):
            result = schema_contract.verify(
                task_id="task-test-sc1",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )
        details_str = "\n".join(result["details"])
        assert "SC-1" in details_str, f"details에 'SC-1' 문자열 없음: {details_str}"

    def test_sc1_details_mention_models_via_mock(self, tmp_path):
        workers_base, schemas_dir, worker_dir = self._make_worker_without_models(tmp_path)
        fake_workers = [{"dir": str(worker_dir), "name": "keyword_worker"}]
        with mock.patch.object(schema_contract, "_find_worker_dirs", return_value=fake_workers):
            result = schema_contract.verify(
                task_id="task-test-sc1",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )
        details_str = "\n".join(result["details"])
        assert "models.py" in details_str, (
            f"details에 'models.py' 언급 없음: {details_str}"
        )

    def test_sc1_no_models_directory_gives_skip(self, tmp_path):
        """
        models.py 없으면 _find_worker_dirs가 Worker를 찾지 못하므로
        실제 verify() 호출 시 SKIP이 반환됨을 확인.
        """
        workers_base, schemas_dir, _ = self._make_worker_without_models(tmp_path)
        result = schema_contract.verify(
            task_id="task-test-sc1-skip",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "SKIP", (
            f"models.py 없어서 Worker 미감지 → SKIP 기대, got: {result}"
        )


# ---------------------------------------------------------------------------
# 3. SC-2: tests/test_contract.py 누락
# ---------------------------------------------------------------------------

class TestSC2TestContractMissing:
    """Worker 내 tests/test_contract.py가 없으면 FAIL + SC-2 메시지."""

    def test_sc2_status_fail(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_test_contract=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc2",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "FAIL", (
            f"test_contract.py 누락 시 FAIL 기대, got: {result}"
        )

    def test_sc2_details_contain_sc2(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_test_contract=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc2",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-2" in details_str, (
            f"details에 'SC-2' 문자열 없음: {details_str}"
        )

    def test_sc2_details_mention_test_contract(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_test_contract=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc2",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "test_contract.py" in details_str, (
            f"details에 'test_contract.py' 언급 없음: {details_str}"
        )

    def test_sc2_fail_message_includes_missing(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_test_contract=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc2",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "MISSING" in details_str or "FAIL" in details_str, (
            f"SC-2 FAIL 메시지에 MISSING/FAIL 없음: {details_str}"
        )


# ---------------------------------------------------------------------------
# 4. SC-3: schema.json 누락
# ---------------------------------------------------------------------------

class TestSC3SchemaMissing:
    """shared/schemas/{name}.schema.json 파일이 없으면 FAIL + SC-3 메시지."""

    def test_sc3_status_fail(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_schema=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc3",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "FAIL", (
            f"schema.json 누락 시 FAIL 기대, got: {result}"
        )

    def test_sc3_details_contain_sc3(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_schema=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc3",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-3" in details_str, (
            f"details에 'SC-3' 문자열 없음: {details_str}"
        )

    def test_sc3_details_mention_schema_json(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_schema=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc3",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "schema.json" in details_str or "schema" in details_str.lower(), (
            f"details에 schema 관련 언급 없음: {details_str}"
        )

    def test_sc3_fail_causes_sc4_sc5_skip(self, tmp_path):
        """SC-3 실패 시 SC-4, SC-5는 SKIP 처리돼야 한다."""
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_schema=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc3-cascade",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        # SC-3 실패 후 SC-4, SC-5는 SKIP이어야 함
        assert "SC-4 SKIP" in details_str or "SC-4 FAIL" in details_str, (
            f"SC-3 실패 후 SC-4가 SKIP 또는 cascade FAIL이어야 함: {details_str}"
        )


# ---------------------------------------------------------------------------
# 5. SC-4 / SC-5: sample 데이터가 JSON Schema와 불일치
# ---------------------------------------------------------------------------

class TestSC4SC5SampleSchemaMismatch:
    """
    sample.normal.json 또는 sample.edge.json이 schema.json을 위반하면
    FAIL + SC-4 혹은 SC-5 메시지가 포함돼야 한다.

    sample 파일 위치: schemas_dir/{worker_name}.sample.normal.json
    """

    def test_sc4_normal_sample_type_mismatch_fail(self, tmp_path):
        """normal sample의 keyword_count가 integer가 아닌 string → SC-4 FAIL"""
        bad_normal = {"keyword": "test", "keyword_count": "not-an-int"}
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, normal_sample=bad_normal
        )
        result = schema_contract.verify(
            task_id="task-test-sc4",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "FAIL", (
            f"normal sample 타입 불일치 시 FAIL 기대, got: {result}"
        )

    def test_sc4_details_contain_sc4(self, tmp_path):
        bad_normal = {"keyword": "test", "keyword_count": "not-an-int"}
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, normal_sample=bad_normal
        )
        result = schema_contract.verify(
            task_id="task-test-sc4",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-4" in details_str, (
            f"normal sample 불일치 시 details에 'SC-4' 없음: {details_str}"
        )

    def test_sc4_details_contain_fail_message(self, tmp_path):
        bad_normal = {"keyword": "test", "keyword_count": "not-an-int"}
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, normal_sample=bad_normal
        )
        result = schema_contract.verify(
            task_id="task-test-sc4",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-4 FAIL" in details_str, (
            f"SC-4 실패 시 'SC-4 FAIL' 메시지 없음: {details_str}"
        )

    def test_sc5_edge_sample_missing_required_field_fail(self, tmp_path):
        """edge sample에서 required 필드(keyword_count) 누락 → SC-5 FAIL"""
        bad_edge = {"keyword": "edge-only"}  # keyword_count 누락
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, edge_sample=bad_edge
        )
        result = schema_contract.verify(
            task_id="task-test-sc5",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "FAIL", (
            f"edge sample 필드 누락 시 FAIL 기대, got: {result}"
        )

    def test_sc5_details_contain_sc5(self, tmp_path):
        bad_edge = {"keyword": "edge-only"}
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, edge_sample=bad_edge
        )
        result = schema_contract.verify(
            task_id="task-test-sc5",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-5" in details_str, (
            f"edge sample 불일치 시 details에 'SC-5' 없음: {details_str}"
        )

    def test_sc5_edge_sample_not_found_is_warn(self, tmp_path):
        """edge sample 파일 자체가 없으면 FAIL이 아닌 WARN (SC-5 WARN)."""
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_edge_sample=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc5-missing",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        # edge sample 없으면 SC-5 WARN (FAIL이 아님)
        assert "SC-5 WARN" in details_str, (
            f"edge sample 파일 없으면 SC-5 WARN 기대: {details_str}"
        )
        # 전체 상태는 WARN 또는 PASS (FAIL이 아님)
        assert result["status"] in ("WARN", "PASS"), (
            f"edge sample 누락은 WARN/PASS 기대, got: {result['status']}"
        )

    def test_sc4_normal_sample_extra_field_fails_if_no_additional(self, tmp_path):
        """additionalProperties: false 스키마에서 extra 필드 포함 normal sample → SC-4 FAIL"""
        bad_normal = {"keyword": "test", "keyword_count": 1, "extra_field": "oops"}
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, normal_sample=bad_normal
        )
        result = schema_contract.verify(
            task_id="task-test-sc4-extra",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        # additionalProperties: false 이므로 FAIL이어야 함
        assert result["status"] == "FAIL", (
            f"extra 필드 포함 normal sample + additionalProperties:false → FAIL 기대, got: {result}"
        )

    def test_sc4_normal_sample_missing_file_is_fail(self, tmp_path):
        """normal sample 파일 자체가 없으면 SC-4 FAIL."""
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_normal_sample=False
        )
        result = schema_contract.verify(
            task_id="task-test-sc4-no-file",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SC-4 FAIL" in details_str, (
            f"normal sample 없으면 SC-4 FAIL 기대: {details_str}"
        )
        assert result["status"] == "FAIL"


# ---------------------------------------------------------------------------
# 6. SC-6: Pydantic 모델 필드 vs JSON Schema 필드 불일치 (mock 활용)
# ---------------------------------------------------------------------------

class TestSC6PydanticSchemaFieldMismatch:
    """
    Pydantic 모델에 정의된 필드와 JSON Schema에 정의된 필드가 다를 때
    FAIL + SC-6 메시지가 있어야 한다.

    _get_pydantic_fields()를 mock하여 실제 동적 임포트 없이 시뮬레이션한다.
    반환값: (fields_set | None, detail_msg)
    """

    def test_sc6_pydantic_extra_field_status_fail(self, tmp_path):
        """Pydantic 모델에 JSON Schema에 없는 extra 필드 존재 → SC-6 FAIL."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)

        # JSON Schema: keyword, keyword_count (2개)
        # Pydantic 모델: keyword, keyword_count, extra_pydantic_field (3개) → 불일치
        mock_fields = ({"keyword", "keyword_count", "extra_pydantic_field"}, "mock import OK")
        with mock.patch.object(schema_contract, "_get_pydantic_fields", return_value=mock_fields):
            result = schema_contract.verify(
                task_id="task-test-sc6-extra",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

        assert result["status"] == "FAIL", (
            f"Pydantic 모델 extra 필드 불일치 시 FAIL 기대, got: {result}"
        )

    def test_sc6_pydantic_extra_field_details_contain_sc6(self, tmp_path):
        """SC-6 FAIL 시 details에 'SC-6 FAIL' 포함."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        mock_fields = ({"keyword", "keyword_count", "extra_pydantic_field"}, "mock import OK")
        with mock.patch.object(schema_contract, "_get_pydantic_fields", return_value=mock_fields):
            result = schema_contract.verify(
                task_id="task-test-sc6",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )
        details_str = "\n".join(result["details"])
        assert "SC-6" in details_str, (
            f"Pydantic 불일치 시 details에 'SC-6' 없음: {details_str}"
        )
        assert "FAIL" in details_str, (
            f"SC-6 불일치 시 'FAIL' 문자열 없음: {details_str}"
        )

    def test_sc6_schema_missing_field_status_fail(self, tmp_path):
        """JSON Schema에 있는 필드가 Pydantic 모델에 없음 → SC-6 FAIL."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)

        # Pydantic 모델에 keyword만 있고 keyword_count 없음
        mock_fields = ({"keyword"}, "mock import OK")
        with mock.patch.object(schema_contract, "_get_pydantic_fields", return_value=mock_fields):
            result = schema_contract.verify(
                task_id="task-test-sc6-missing",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

        assert result["status"] == "FAIL", (
            f"Pydantic 모델 필드 부족 시 FAIL 기대, got: {result}"
        )

    def test_sc6_matching_fields_pass(self, tmp_path):
        """Pydantic 필드가 JSON Schema 필드와 완전히 일치하면 SC-6 PASS."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)

        # JSON Schema 필드와 동일하게 반환
        mock_fields = ({"keyword", "keyword_count"}, "mock import OK")
        with mock.patch.object(schema_contract, "_get_pydantic_fields", return_value=mock_fields):
            result = schema_contract.verify(
                task_id="task-test-sc6-pass",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

        details_str = "\n".join(result["details"])
        assert "SC-6 PASS" in details_str, (
            f"Pydantic 필드 일치 시 SC-6 PASS 기대: {details_str}"
        )

    def test_sc6_import_failure_is_fail(self, tmp_path):
        """Pydantic 모델 동적 임포트 실패 시 SC-6 FAIL."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)

        # _get_pydantic_fields가 (None, error_msg) 반환 → 임포트 실패 시뮬레이션
        mock_fields = (None, "Dynamic import FAILED: SomeError")
        with mock.patch.object(schema_contract, "_get_pydantic_fields", return_value=mock_fields):
            result = schema_contract.verify(
                task_id="task-test-sc6-import-fail",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

        details_str = "\n".join(result["details"])
        assert "SC-6 FAIL" in details_str, (
            f"Pydantic 임포트 실패 시 SC-6 FAIL 기대: {details_str}"
        )
        assert result["status"] == "FAIL"


# ---------------------------------------------------------------------------
# 7. workers/ 디렉토리 자체 없음 → SKIP
# ---------------------------------------------------------------------------

class TestWorkersDirectoryMissing:
    """workers/ 디렉토리가 아예 없으면 SKIP을 반환해야 한다."""

    def test_no_workers_dir_status_skip(self, tmp_path):
        """workers/ 디렉토리 자체가 없으면 SKIP."""
        schemas_dir = tmp_path / "schemas"
        schemas_dir.mkdir(parents=True)

        result = schema_contract.verify(
            task_id="task-test-no-workers",
            workers_base_dir=str(tmp_path / "workers"),  # 존재하지 않는 경로
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "SKIP", (
            f"workers/ 디렉토리 없으면 SKIP 기대, got: {result}"
        )

    def test_no_workers_dir_details_mention_reason(self, tmp_path):
        """SKIP 시 details에 그 이유가 명시돼야 한다."""
        schemas_dir = tmp_path / "schemas"
        schemas_dir.mkdir(parents=True)

        result = schema_contract.verify(
            task_id="task-test-no-workers",
            workers_base_dir=str(tmp_path / "workers"),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert len(result["details"]) > 0, "SKIP이어도 details에 이유 필요"
        # SKIP 이유가 포함돼야 함
        assert any(
            kw in details_str.lower()
            for kw in ("not found", "없", "skip", "missing", "workers", "no workers")
        ), f"SKIP 이유가 details에 없음: {details_str}"

    def test_empty_workers_dir_status_skip(self, tmp_path):
        """workers/ 디렉토리가 있지만 models.py를 가진 하위 디렉토리가 없으면 SKIP."""
        workers_base = tmp_path / "workers"
        workers_base.mkdir(parents=True)  # 빈 디렉토리
        schemas_dir = tmp_path / "schemas"
        schemas_dir.mkdir(parents=True)

        result = schema_contract.verify(
            task_id="task-test-empty-workers",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "SKIP", (
            f"models.py 없는 빈 workers/ → SKIP 기대, got: {result}"
        )

    def test_no_workers_dir_details_contain_skip_message(self, tmp_path):
        """SKIP 반환 시 details에 'SKIP' 문자열 포함."""
        schemas_dir = tmp_path / "schemas"
        schemas_dir.mkdir(parents=True)

        result = schema_contract.verify(
            task_id="task-test-no-workers-skip-msg",
            workers_base_dir=str(tmp_path / "nonexistent"),
            schemas_dir=str(schemas_dir),
        )
        details_str = "\n".join(result["details"])
        assert "SKIP" in details_str, f"SKIP 반환 시 details에 'SKIP' 없음: {details_str}"


# ---------------------------------------------------------------------------
# 8. jsonschema 라이브러리 부재 → SC-4/SC-5 SKIP + WARN
#
# 구현 확인 결과: jsonschema 없을 때 전체 SKIP이 아니라
# WARN 메시지 + SC-4/SC-5 개별 SKIP 처리됨.
# 전체 status는 다른 항목 결과에 따라 PASS/WARN/FAIL이 될 수 있음.
# ---------------------------------------------------------------------------

class TestJsonschemaMissing:
    """jsonschema 패키지가 없을 때 SC-4/SC-5 SKIP + 설치 안내 메시지."""

    def _verify_without_jsonschema(self, tmp_path: Path) -> dict:
        """jsonschema를 None으로 mock하고 verify() 호출."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        with mock.patch.object(
            schema_contract,
            "_try_import_jsonschema",
            return_value=None,
        ):
            return schema_contract.verify(
                task_id="task-test-no-jsonschema",
                workers_base_dir=str(workers_base),
                schemas_dir=str(schemas_dir),
            )

    def test_no_jsonschema_sc4_skipped(self, tmp_path):
        """jsonschema 없으면 SC-4 SKIP."""
        result = self._verify_without_jsonschema(tmp_path)
        details_str = "\n".join(result["details"])
        assert "SC-4 SKIP" in details_str, (
            f"jsonschema 없으면 SC-4 SKIP 기대: {details_str}"
        )

    def test_no_jsonschema_sc5_skipped(self, tmp_path):
        """jsonschema 없으면 SC-5 SKIP."""
        result = self._verify_without_jsonschema(tmp_path)
        details_str = "\n".join(result["details"])
        assert "SC-5 SKIP" in details_str, (
            f"jsonschema 없으면 SC-5 SKIP 기대: {details_str}"
        )

    def test_no_jsonschema_details_contain_install_guide(self, tmp_path):
        """설치 안내 메시지(pip install jsonschema)가 details에 포함돼야 한다."""
        result = self._verify_without_jsonschema(tmp_path)
        details_str = "\n".join(result["details"])
        assert any(
            kw in details_str.lower()
            for kw in ("pip install jsonschema", "jsonschema", "install")
        ), f"설치 안내 메시지 없음: {details_str}"

    def test_no_jsonschema_details_not_empty(self, tmp_path):
        """jsonschema 없어도 details에 내용이 있어야 한다."""
        result = self._verify_without_jsonschema(tmp_path)
        assert len(result["details"]) > 0, "details가 비어있음"

    def test_no_jsonschema_status_not_fail_from_sc4_sc5(self, tmp_path):
        """
        jsonschema 없으면 SC-4/SC-5는 SKIP이지 FAIL이 아니므로
        다른 항목이 모두 PASS면 전체 status는 PASS 또는 WARN.
        """
        result = self._verify_without_jsonschema(tmp_path)
        assert result["status"] in ("PASS", "WARN"), (
            f"jsonschema 없을 때 SC-4/SC-5 SKIP이면 전체 PASS/WARN 기대, got: {result['status']}"
        )

    def test_no_jsonschema_warn_message_in_details(self, tmp_path):
        """jsonschema 없으면 WARN 경고가 details 상단에 표시돼야 한다."""
        result = self._verify_without_jsonschema(tmp_path)
        details_str = "\n".join(result["details"])
        assert "WARN" in details_str, (
            f"jsonschema 없으면 WARN 메시지 기대: {details_str}"
        )


# ---------------------------------------------------------------------------
# 9. 반환 형식 공통 규약 검증
# ---------------------------------------------------------------------------

class TestReturnFormatContract:
    """verify() 반환 딕셔너리가 항상 규약된 형식을 따르는지 확인."""

    def test_status_is_valid_enum(self, tmp_path):
        """status는 PASS|FAIL|WARN|SKIP 중 하나여야 한다."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-format-check",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] in {"PASS", "FAIL", "WARN", "SKIP"}, (
            f"status가 유효한 enum 값이 아님: {result['status']}"
        )

    def test_details_is_list_of_strings(self, tmp_path):
        """details는 문자열 리스트여야 한다."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-format-check",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert isinstance(result["details"], list), "details가 list가 아님"
        for item in result["details"]:
            assert isinstance(item, str), f"details 항목이 str이 아님: {item!r}"

    def test_has_status_key(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-format-check",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert "status" in result

    def test_has_details_key(self, tmp_path):
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-format-check",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert "details" in result

    def test_no_extra_unexpected_keys(self, tmp_path):
        """반환 딕셔너리에는 status, details 외 추가 키가 없거나 알려진 키만 있어야 한다."""
        workers_base, schemas_dir = _make_worker_dir(tmp_path)
        result = schema_contract.verify(
            task_id="task-format-check",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        allowed_keys = {"status", "details", "summary", "checked_workers", "errors"}
        unexpected = set(result.keys()) - allowed_keys
        assert not unexpected, f"예상치 못한 키 발견: {unexpected}"

    def test_skip_status_when_no_workers(self, tmp_path):
        """workers가 없으면 반드시 SKIP이 반환돼야 한다."""
        schemas_dir = tmp_path / "schemas"
        schemas_dir.mkdir()
        result = schema_contract.verify(
            task_id="task-format-skip",
            workers_base_dir=str(tmp_path / "nonexistent"),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "SKIP"

    def test_fail_status_propagates_from_worker(self, tmp_path):
        """Worker 하나라도 FAIL이면 전체 status가 FAIL이어야 한다."""
        workers_base, schemas_dir = _make_worker_dir(
            tmp_path, include_test_contract=False  # SC-2 FAIL 유발
        )
        result = schema_contract.verify(
            task_id="task-format-fail-propagate",
            workers_base_dir=str(workers_base),
            schemas_dir=str(schemas_dir),
        )
        assert result["status"] == "FAIL", (
            f"Worker FAIL → 전체 FAIL 기대, got: {result['status']}"
        )
