"""
image-skill-router.py 유닛 테스트

테스트 범위:
  - route_skill: 작업 유형별 스킬 라우팅
  - route_skill: urgent=True 오버라이드
  - route_skill: count >= 10 강제 라우팅
  - route_skill: 알 수 없는 작업 유형 처리
  - route_skill: 출력 dict 구조 검증
  - get_skill_recommendation: 작업 설명 키워드 기반 스킬 추천
  - CLI 통합: subprocess 기반 end-to-end 실행

외부 의존성:
  - subprocess → CLI 통합 테스트
  - 나머지는 모듈 직접 호출
"""

from __future__ import annotations

import importlib.util as _ilu
import json
import subprocess
import sys
import types
import pytest

# ---------------------------------------------------------------------------
# 모듈 임포트
# ---------------------------------------------------------------------------

_MODULE_PATH = "/home/jay/workspace/tools/image-skill-router.py"

try:
    _spec = _ilu.spec_from_file_location("image_skill_router", _MODULE_PATH)
    image_skill_router = _ilu.module_from_spec(_spec)  # type: ignore[arg-type]
    _spec.loader.exec_module(image_skill_router)  # type: ignore[union-attr]
except Exception as exc:  # pragma: no cover
    image_skill_router = types.ModuleType("image_skill_router_stub")
    _IMPORT_ERROR: Exception | None = exc
else:
    _IMPORT_ERROR = None

pytestmark = pytest.mark.skipif(
    _IMPORT_ERROR is not None,
    reason=f"image-skill-router.py 임포트 실패: {_IMPORT_ERROR}",
)

# 유효한 스킬 이름 집합
VALID_SKILLS = {"hybrid-image", "satori-cardnews", "gemini-image"}

# 출력 dict 필수 키 집합
REQUIRED_KEYS = {"recommended_skill", "reason", "alternatives", "warnings", "input_type"}


# ---------------------------------------------------------------------------
# 헬퍼
# ---------------------------------------------------------------------------


def _route(task_type: str, **kwargs) -> dict:
    """route_skill 호출 헬퍼."""
    return image_skill_router.route_skill(task_type, **kwargs)


# ---------------------------------------------------------------------------
# 1. 기본 라우팅 테스트
# ---------------------------------------------------------------------------


class TestBasicRouting:
    """각 작업 유형별 올바른 스킬 추천 검증."""

    @pytest.mark.parametrize(
        "task_type, expected_skill",
        [
            ("광고 배너", "hybrid-image"),
            ("카드뉴스", "satori-cardnews"),
            ("SNS 메인 이미지", "gemini-image"),
            ("블로그 썸네일", "hybrid-image"),
            ("A/B 테스트 변형", "satori-cardnews"),
            ("프리미엄 브랜딩", "gemini-image"),
            ("인포그래픽", "satori-cardnews"),
            ("비교표", "satori-cardnews"),
            ("Meta 배너", "hybrid-image"),
            ("Google 정적 배너", "satori-cardnews"),
            ("네이버 GFA Smart", "satori-cardnews"),
            ("카카오 비즈보드", "satori-cardnews"),
        ],
    )
    def test_routing_by_task_type(self, task_type: str, expected_skill: str) -> None:
        """작업 유형에 따라 올바른 스킬이 추천된다."""
        result = _route(task_type)
        assert result["recommended_skill"] == expected_skill, (
            f"task_type={task_type!r}: "
            f"expected {expected_skill!r}, got {result['recommended_skill']!r}"
        )


# ---------------------------------------------------------------------------
# 2. 긴급 모드 테스트
# ---------------------------------------------------------------------------


class TestUrgentMode:
    """urgent=True 시 satori-cardnews 강제 및 경고 포함 검증."""

    def test_urgent_true_returns_satori(self) -> None:
        """urgent=True 이면 추천 스킬이 satori-cardnews 이다."""
        result = _route("광고 배너", urgent=True)
        assert result["recommended_skill"] == "satori-cardnews"

    def test_urgent_true_includes_warning(self) -> None:
        """urgent=True 이면 warnings 리스트에 경고가 포함된다."""
        result = _route("광고 배너", urgent=True)
        assert isinstance(result["warnings"], list)
        assert len(result["warnings"]) > 0, "urgent 모드 경고가 없습니다."

    def test_urgent_overrides_gemini_recommendation(self) -> None:
        """원래 gemini-image가 추천될 작업도 urgent=True면 satori로 오버라이드된다."""
        # 기본(non-urgent)에서 gemini 추천 확인
        normal_result = _route("프리미엄 브랜딩")
        assert normal_result["recommended_skill"] == "gemini-image", (
            "이 테스트는 '프리미엄 브랜딩'이 gemini-image를 반환해야 성립합니다."
        )

        # urgent=True에서 satori로 오버라이드 확인
        urgent_result = _route("프리미엄 브랜딩", urgent=True)
        assert urgent_result["recommended_skill"] == "satori-cardnews"

    def test_urgent_overrides_hybrid_recommendation(self) -> None:
        """원래 hybrid-image가 추천될 작업도 urgent=True면 satori로 오버라이드된다."""
        normal_result = _route("광고 배너")
        assert normal_result["recommended_skill"] == "hybrid-image", (
            "이 테스트는 '광고 배너'이 hybrid-image를 반환해야 성립합니다."
        )

        urgent_result = _route("광고 배너", urgent=True)
        assert urgent_result["recommended_skill"] == "satori-cardnews"

    def test_urgent_false_does_not_force_satori(self) -> None:
        """urgent=False(기본값)이면 강제 오버라이드가 없다."""
        result = _route("광고 배너", urgent=False)
        assert result["recommended_skill"] == "hybrid-image"


# ---------------------------------------------------------------------------
# 3. 대량 생성 테스트
# ---------------------------------------------------------------------------


class TestBulkGeneration:
    """count 값에 따른 satori 강제 및 정상 라우팅 검증."""

    def test_count_10_forces_satori(self) -> None:
        """count >= 10 이면 satori-cardnews가 강제된다."""
        result = _route("광고 배너", count=10)
        assert result["recommended_skill"] == "satori-cardnews"

    def test_count_100_forces_satori(self) -> None:
        """count=100 같은 대량 생성도 satori-cardnews를 반환한다."""
        result = _route("프리미엄 브랜딩", count=100)
        assert result["recommended_skill"] == "satori-cardnews"

    def test_count_9_normal_routing(self) -> None:
        """count=9 이면 정상 라우팅 결과를 반환한다."""
        result = _route("광고 배너", count=9)
        assert result["recommended_skill"] == "hybrid-image"

    def test_count_1_normal_routing(self) -> None:
        """count=1(기본값)이면 정상 라우팅 결과를 반환한다."""
        result = _route("카드뉴스", count=1)
        assert result["recommended_skill"] == "satori-cardnews"

    def test_count_default_normal_routing(self) -> None:
        """count를 명시하지 않으면 정상 라우팅이 적용된다."""
        result = _route("SNS 메인 이미지")
        assert result["recommended_skill"] == "gemini-image"


# ---------------------------------------------------------------------------
# 4. 매칭 실패 테스트
# ---------------------------------------------------------------------------


class TestUnknownTaskType:
    """알 수 없는 작업 유형 입력 시 처리 검증."""

    def test_unknown_task_type_returns_dict(self) -> None:
        """알 수 없는 작업 유형에서도 dict를 반환한다."""
        result = _route("완전히 알 수 없는 작업 유형 xyz")
        assert isinstance(result, dict), "dict를 반환해야 합니다."

    def test_unknown_task_type_returns_none_or_valid_skill(self) -> None:
        """알 수 없는 작업 유형이면 recommended_skill이 None이거나 유효한 스킬이다."""
        result = _route("알수없음abcXYZ123")
        skill = result["recommended_skill"]
        assert skill is None or skill in VALID_SKILLS, (
            f"반환된 스킬 {skill!r}이 None도 유효한 스킬도 아닙니다."
        )

    def test_empty_string_task_type(self) -> None:
        """빈 문자열 작업 유형도 처리 가능하다."""
        try:
            result = _route("")
            assert isinstance(result, dict)
        except (ValueError, KeyError):
            pass

    def test_unknown_task_type_has_required_keys_if_returns_dict(self) -> None:
        """알 수 없는 작업 유형에서 dict를 반환할 경우 필수 키가 모두 포함된다."""
        try:
            result = _route("알수없음XYZ")
            if isinstance(result, dict):
                missing = REQUIRED_KEYS - set(result.keys())
                assert not missing, f"필수 키 누락: {missing}"
        except (ValueError, KeyError, SystemExit):
            pass


# ---------------------------------------------------------------------------
# 5. 출력 구조 검증
# ---------------------------------------------------------------------------


class TestOutputStructure:
    """route_skill 반환 dict의 구조 및 값 타입 검증."""

    @pytest.fixture
    def sample_result(self) -> dict:
        """테스트용 샘플 결과 (광고 배너)."""
        return _route("광고 배너")

    def test_required_keys_present(self, sample_result: dict) -> None:
        """반환 dict에 필수 키가 모두 존재한다."""
        missing = REQUIRED_KEYS - set(sample_result.keys())
        assert not missing, f"필수 키 누락: {missing}"

    def test_recommended_skill_is_valid(self, sample_result: dict) -> None:
        """recommended_skill 값이 유효한 스킬 이름이다."""
        assert sample_result["recommended_skill"] in VALID_SKILLS

    def test_reason_is_string(self, sample_result: dict) -> None:
        """reason 값이 문자열이다."""
        assert isinstance(sample_result["reason"], str)
        assert len(sample_result["reason"]) > 0, "reason이 빈 문자열입니다."

    def test_alternatives_is_list(self, sample_result: dict) -> None:
        """alternatives 값이 리스트이다."""
        assert isinstance(sample_result["alternatives"], list)

    def test_alternatives_contains_valid_skills(self, sample_result: dict) -> None:
        """alternatives의 각 항목이 유효한 스킬 이름이다."""
        for alt in sample_result["alternatives"]:
            assert alt in VALID_SKILLS, f"유효하지 않은 대안 스킬: {alt!r}"

    def test_warnings_is_list(self, sample_result: dict) -> None:
        """warnings 값이 리스트이다."""
        assert isinstance(sample_result["warnings"], list)

    def test_input_type_is_string(self, sample_result: dict) -> None:
        """input_type 값이 문자열이다."""
        assert isinstance(sample_result["input_type"], str)

    def test_recommended_skill_not_in_alternatives(self, sample_result: dict) -> None:
        """recommended_skill이 alternatives에 포함되지 않는다."""
        assert sample_result["recommended_skill"] not in sample_result["alternatives"], (
            "추천된 스킬이 대안 목록에도 포함되어 있습니다."
        )

    @pytest.mark.parametrize(
        "task_type",
        ["광고 배너", "카드뉴스", "SNS 메인 이미지", "인포그래픽", "비교표"],
    )
    def test_all_task_types_have_required_keys(self, task_type: str) -> None:
        """다양한 작업 유형에서 모두 필수 키가 존재한다."""
        result = _route(task_type)
        missing = REQUIRED_KEYS - set(result.keys())
        assert not missing, f"task_type={task_type!r}: 필수 키 누락: {missing}"


# ---------------------------------------------------------------------------
# 6. get_skill_recommendation 테스트
# ---------------------------------------------------------------------------


class TestGetSkillRecommendation:
    """get_skill_recommendation 함수: 작업 설명 키워드 기반 추천 검증."""

    def test_meta_ad_banner_returns_hybrid_image(self) -> None:
        """'메타 광고 배너 제작' → hybrid-image."""
        result = image_skill_router.get_skill_recommendation("메타 광고 배너 제작")
        assert result == "hybrid-image"

    def test_insurance_cardnews_returns_satori(self) -> None:
        """'보험 카드뉴스 제작' → satori-cardnews."""
        result = image_skill_router.get_skill_recommendation("보험 카드뉴스 제작")
        assert result == "satori-cardnews"

    def test_sns_promo_image_returns_gemini(self) -> None:
        """'SNS 홍보 이미지' → gemini-image."""
        result = image_skill_router.get_skill_recommendation("SNS 홍보 이미지")
        assert result == "gemini-image"

    def test_return_type_is_string(self) -> None:
        """반환값이 문자열이다."""
        result = image_skill_router.get_skill_recommendation("광고 배너")
        assert isinstance(result, str)

    def test_return_value_is_valid_skill(self) -> None:
        """반환값이 유효한 스킬 이름이다."""
        result = image_skill_router.get_skill_recommendation("카드뉴스 만들기")
        assert result in VALID_SKILLS


# ---------------------------------------------------------------------------
# 7. CLI 통합 테스트 (subprocess)
# ---------------------------------------------------------------------------


class TestCLIIntegration:
    """subprocess로 CLI 직접 실행하여 통합 동작 검증."""

    _SCRIPT = _MODULE_PATH

    def _run(self, args: list[str], **kwargs) -> subprocess.CompletedProcess:
        """CLI를 subprocess로 실행하고 결과를 반환한다."""
        cmd = [sys.executable, self._SCRIPT] + args
        return subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30,
            **kwargs,
        )

    def test_basic_run_exits_zero(self) -> None:
        """--type '광고 배너' 실행 시 exit code 0을 반환한다."""
        result = self._run(["--type", "광고 배너"])
        assert result.returncode == 0, (
            f"exit code {result.returncode}\nstdout: {result.stdout}\nstderr: {result.stderr}"
        )

    def test_json_output_is_parseable(self) -> None:
        """--json 플래그 사용 시 출력이 유효한 JSON이다."""
        result = self._run(["--type", "광고 배너", "--json"])
        assert result.returncode == 0, (
            f"exit code {result.returncode}\nstderr: {result.stderr}"
        )
        try:
            parsed = json.loads(result.stdout)
        except json.JSONDecodeError as e:
            pytest.fail(f"JSON 파싱 실패: {e}\nstdout: {result.stdout!r}")
        assert isinstance(parsed, dict), "JSON 최상위가 dict여야 합니다."

    def test_json_output_has_required_keys(self) -> None:
        """--json 출력에 필수 키가 모두 포함된다."""
        result = self._run(["--type", "광고 배너", "--json"])
        assert result.returncode == 0
        parsed = json.loads(result.stdout)
        missing = REQUIRED_KEYS - set(parsed.keys())
        assert not missing, f"JSON 출력에 필수 키 누락: {missing}"

    def test_urgent_flag_recommends_satori(self) -> None:
        """--urgent 플래그 사용 시 satori-cardnews가 추천된다."""
        result = self._run(["--type", "광고 배너", "--urgent", "--json"])
        assert result.returncode == 0, (
            f"exit code {result.returncode}\nstderr: {result.stderr}"
        )
        parsed = json.loads(result.stdout)
        assert parsed["recommended_skill"] == "satori-cardnews", (
            f"urgent 모드에서 satori가 아닌 {parsed['recommended_skill']!r} 추천됨"
        )

    def test_no_args_exits_nonzero(self) -> None:
        """인수 없이 실행하면 non-zero exit code를 반환한다."""
        result = self._run([])
        assert result.returncode != 0, (
            "인수 없이 실행했는데 exit code 0이 반환됐습니다."
        )

    def test_cardnews_type_recommends_satori_via_cli(self) -> None:
        """CLI에서 '카드뉴스' 유형 실행 시 satori-cardnews가 추천된다."""
        result = self._run(["--type", "카드뉴스", "--json"])
        assert result.returncode == 0
        parsed = json.loads(result.stdout)
        assert parsed["recommended_skill"] == "satori-cardnews"
