"""IDS Phase 6 — natural-language routing regression tests.

Covers:
- intent classification per category (5 + ambiguous)
- 50-sample accuracy ≥ 90%
- routing matrix dual-version cross-use (satori vs threadauto_render)
- size/style extractors
- external API blocking gate
- confidence + needs_confirmation thresholds
- SLA P95 ≤ 2000ms
- diagnostic logging fields
"""

from __future__ import annotations

import importlib
import sys
from pathlib import Path

import pytest

# Make `scripts/` importable regardless of pytest's cwd.
ROOT = Path(__file__).resolve().parents[2]
SCRIPTS = ROOT / "scripts"
if str(SCRIPTS) not in sys.path:
    sys.path.insert(0, str(SCRIPTS))

ids_router = importlib.import_module("ids_natural_routing")


# ---------- Sample bank (50 prompts) -----------------------------------------------


SAMPLES = [
    # cardnews (10)
    ("인스타그램 카드뉴스 5장 만들어줘", "cardnews"),
    ("카드뉴스 supabase 스타일로", "cardnews"),
    ("페이스북 카드뉴스 3장", "cardnews"),
    ("인포그래픽 한 장 만들어줘", "cardnews"),
    ("카드 뉴스 자동 생성", "cardnews"),
    ("instagram 카드뉴스 디자인", "cardnews"),
    ("배너 만들어줘 인스타용", "cardnews"),
    ("토스 스타일 카드뉴스", "cardnews"),
    ("threads에 올릴 스레드 카드뉴스 8장", "cardnews"),
    ("infographic for instagram", "cardnews"),
    # ppt (10)
    ("supabase 스타일 보험 PPT 5장", "ppt"),
    ("PPT 발표 자료 매거진 스타일", "ppt"),
    ("덱 만들어줘 stripe 스타일", "ppt"),
    ("프레젠테이션 슬라이드 12장", "ppt"),
    ("pitch deck 작성해줘", "ppt"),
    ("발표자료 키노트 스타일", "ppt"),
    ("회사 소개 PPT 만들어줘", "ppt"),
    ("매거진 PPT 보험 상품 소개", "ppt"),
    ("슬라이드 덱 ppt 한글", "ppt"),
    ("presentation slides for chairman", "ppt"),
    # mobile (10)
    ("iPhone 15 Pro 모바일 프로토타입", "mobile"),
    ("아이폰 시연용 모바일 프로토타입", "mobile"),
    ("Pixel 9 Pro 앱 시연 영상", "mobile"),
    ("픽셀 모바일 프로토타입 만들어줘", "mobile"),
    ("앱 화면 모바일 프로토타입 합성", "mobile"),
    ("iphone 베타테스트 시연", "mobile"),
    ("모바일 프로토타입 신기능 보여줘", "mobile"),
    ("프로토타입 iphone 가입 흐름", "mobile"),
    ("Pixel 앱 화면 시연", "mobile"),
    ("mobile prototype 회장 베타", "mobile"),
    # motion (10)
    ("모션 카드뉴스 만들어줘 reels", "motion"),
    ("동영상 카드뉴스 30초", "motion"),
    ("MP4 카드뉴스 fade 효과", "motion"),
    ("쇼츠용 모션 카드뉴스", "motion"),
    ("리얼스 동영상 카드뉴스", "motion"),
    ("html to video 변환", "motion"),
    ("threads 모션 카드뉴스 3초", "motion"),
    ("애니메이션 카드뉴스 BGM", "motion"),
    ("video for instagram reels", "motion"),
    ("모션 카드뉴스 슬라이드 효과", "motion"),
    # image (10)
    ("광고 포토 이미지 한글 카피 가득", "image"),
    ("광고 사진 포토리얼 인스타", "image"),
    ("광고 이미지 facebook용", "image"),
    ("ad image 보험 상품 광고", "image"),
    ("포토리얼 광고 이미지 페북", "image"),
    ("실사 배경 광고 이미지", "image"),
    ("ad photo for facebook insurance", "image"),
    ("배경 이미지 광고용 사진 느낌", "image"),
    ("광고 포토 이미지 텍스트 많은 카피", "image"),
    ("photoreal 광고 이미지 instagram", "image"),
]


def test_sample_count():
    assert len(SAMPLES) == 50


# ---------- 1) per-intent classification --------------------------------------------


@pytest.mark.parametrize(
    "prompt,expected",
    [
        ("인스타그램 카드뉴스 5장", "cardnews"),
        ("supabase 스타일 PPT 5장", "ppt"),
        ("iPhone 15 Pro 모바일 프로토타입", "mobile"),
        ("MP4 모션 카드뉴스 reels", "motion"),
        ("광고 포토 이미지 페북", "image"),
        ("React 랜딩 페이지 코드", "code"),
    ],
)
def test_intent_classification(prompt, expected):
    decision = ids_router.route(prompt)
    assert decision.intent == expected, f"prompt={prompt!r} → {decision.intent}"


# ---------- 2) 50-sample accuracy ≥ 90% ---------------------------------------------


def test_routing_accuracy_50_samples():
    correct = 0
    misses = []
    for prompt, expected in SAMPLES:
        d = ids_router.route(prompt)
        if d.intent == expected:
            correct += 1
        else:
            misses.append((prompt, expected, d.intent))
    accuracy = correct / len(SAMPLES)
    assert accuracy >= 0.90, f"accuracy={accuracy:.2%}; misses={misses}"


# ---------- 3) routing matrix per category ------------------------------------------


def test_route_ppt_to_magazine_skill():
    d = ids_router.route("supabase 스타일 보험 PPT 5장")
    assert d.skill == "magazine-ppt-ko"


def test_route_mobile_to_mobile_prototype_skill():
    d = ids_router.route("iPhone 15 Pro 모바일 프로토타입 시연")
    assert d.skill == "mobile-prototype-ko"


def test_route_motion_to_motion_skill():
    d = ids_router.route("MP4 모션 카드뉴스 reels 30초")
    assert d.skill == "motion-cardnews-ko"


def test_route_code_to_frontend_design():
    d = ids_router.route("토스 스타일 React 랜딩 페이지 컴포넌트")
    assert d.skill == "frontend-design"


# ---------- 4) cardnews dual-version cross-use --------------------------------------


def test_cardnews_short_body_uses_satori_primary():
    d = ids_router.route("인스타 카드뉴스 1장")
    assert d.intent == "cardnews"
    assert d.skill == "satori-cardnews"
    assert d.fallback_skill == "threadauto_render"


def test_cardnews_long_or_thread_uses_threadauto_primary():
    long_prompt = (
        "스레드에 올릴 카드뉴스 8장. 본문은 길게 — 보험 가입 흐름을 설명하고, "
        "각 단계별 카피를 자세히 풀어서 작성해줘 supabase 스타일"
    )
    d = ids_router.route(long_prompt)
    assert d.intent == "cardnews"
    assert d.skill == "threadauto_render"
    assert d.fallback_skill == "satori-cardnews"


# ---------- 5) image: hybrid vs gemini ----------------------------------------------


def test_image_heavy_text_uses_hybrid():
    d = ids_router.route("광고 포토 이미지 한글 텍스트 많은 카피")
    assert d.intent == "image"
    assert d.skill == "hybrid-image"
    assert d.fallback_skill == "gemini-image"


def test_image_default_uses_gemini():
    d = ids_router.route("광고 사진 포토리얼 인스타")
    assert d.intent == "image"
    assert d.skill == "gemini-image"
    assert d.fallback_skill == "hybrid-image"


# ---------- 6) external API direct-call blocking gate -------------------------------


def test_block_direct_api_for_image_when_caller_not_design_team():
    with pytest.raises(ids_router.RoutingError):
        ids_router.route("광고 포토 이미지 페북", caller="external_caller")


def test_block_direct_api_call_helper():
    # explicit unit on the gate function itself
    with pytest.raises(ids_router.RoutingError):
        ids_router.block_direct_api_call("ppt", caller="dispatch")
    # design_team caller must pass
    ids_router.block_direct_api_call("ppt", caller="design_team")
    # non-design-team intent (e.g. code) should not require gate
    ids_router.block_direct_api_call("code", caller="anything")


# ---------- 7) confidence + needs_confirmation --------------------------------------


def test_high_confidence_does_not_need_confirmation():
    d = ids_router.route("supabase 스타일 매거진 발표자료 PPT 슬라이드 덱 12장")
    assert d.confidence >= ids_router.AUTO_CONFIDENCE
    assert d.needs_confirmation is False
    assert d.confirm_message is None


def test_ambiguous_prompt_returns_ambiguous_intent():
    d = ids_router.route("뭔가 만들어줘 멋있게")
    assert d.intent == "ambiguous"
    assert d.needs_confirmation is True
    assert d.confirm_message is not None


# ---------- 8) size + style extractors ----------------------------------------------


def test_extracts_size_instagram():
    d = ids_router.route("인스타그램 카드뉴스 만들어줘")
    assert d.size == "instagram_square"


def test_extracts_style_brand():
    d = ids_router.route("supabase 스타일 PPT 5장")
    assert d.style == "supabase"


# ---------- 9) SLA P95 ≤ 2000ms -----------------------------------------------------


def test_routing_sla_p95():
    elapsed = []
    for prompt, _ in SAMPLES:
        d = ids_router.route(prompt)
        elapsed.append(d.elapsed_ms)
        assert d.sla_ok, f"sla_ok=False for {prompt!r} ({d.elapsed_ms}ms)"
    elapsed.sort()
    p95 = elapsed[int(len(elapsed) * 0.95)]
    assert p95 <= ids_router.SLA_ROUTE_MS, f"P95={p95}ms exceeds {ids_router.SLA_ROUTE_MS}ms"


# ---------- 10) diagnostic + PII-safe logging ---------------------------------------


def test_diagnostic_fields_present():
    d = ids_router.route("PPT 발표자료 매거진")
    assert d.diagnostic["phase"] == "route"
    assert "model_quality" in d.diagnostic
    assert "cli_constraint" in d.diagnostic
    assert "scores" in d.diagnostic
    assert isinstance(d.diagnostic["body_chars"], int)


def test_prompt_hash_does_not_leak_raw_prompt():
    raw = "주민등록번호 901010-1234567 카드뉴스 인스타"
    d = ids_router.route(raw)
    # hash is 16 hex chars and must NOT contain the raw prompt content
    assert len(d.prompt_hash) == 16
    assert "주민등록번호" not in d.prompt_hash
    assert "901010" not in d.prompt_hash
    serialized = d.to_json()
    assert raw not in serialized


# ---------- 11) edge cases ----------------------------------------------------------


def test_empty_prompt_raises():
    with pytest.raises(ids_router.RoutingError):
        ids_router.route("")


def test_intent_set_complete():
    assert set(ids_router.INTENTS) == {
        "cardnews",
        "ppt",
        "mobile",
        "motion",
        "image",
        "code",
        "ambiguous",
    }


def test_routing_accuracy_per_category():
    """Smoke-check: each category has at least 80% accuracy in its sub-bucket."""

    by_intent: dict = {}
    for prompt, expected in SAMPLES:
        by_intent.setdefault(expected, []).append(prompt)
    for intent, prompts in by_intent.items():
        correct = sum(1 for p in prompts if ids_router.route(p).intent == intent)
        assert correct / len(prompts) >= 0.8, (
            f"intent={intent} per-bucket accuracy {correct}/{len(prompts)} below 0.80"
        )


def test_to_json_round_trip():
    import json

    d = ids_router.route("인스타 카드뉴스")
    parsed = json.loads(d.to_json())
    assert parsed["intent"] == "cardnews"
    assert parsed["skill"] in ("satori-cardnews", "threadauto_render")
    assert "elapsed_ms" in parsed
