"""IDS Phase 2 — magazine-ppt-ko 회귀 테스트 (G2 게이트).

self-contained: conftest 없이도 수집/실행 가능.
- 외부 API mock 차단 (정적 검사)
- 한글 100% string-match
- PPTX 생성 + 한글 추출 검증

python-pptx 미설치 시 PPTX 관련 케이스는 자동 skip.
"""

from __future__ import annotations

import importlib.util
import json
import re
import sys
from pathlib import Path
from typing import Any

import pytest

# --- 경로/임포트 --------------------------------------------------------------
SKILL_ROOT = Path("/home/jay/workspace/skills/magazine-ppt-ko")
SCRIPTS_DIR = SKILL_ROOT / "scripts"
TEMPLATES_DIR = SKILL_ROOT / "templates"

if str(SCRIPTS_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPTS_DIR))


def _import_module(name: str) -> Any:
    """scripts/ 디렉토리의 모듈을 importlib으로 안전하게 로드."""
    spec = importlib.util.spec_from_file_location(name, SCRIPTS_DIR / f"{name}.py")
    if spec is None or spec.loader is None:
        raise ImportError(f"cannot load {name}")
    module = importlib.util.module_from_spec(spec)
    sys.modules[name] = module
    spec.loader.exec_module(module)
    return module


build_deck = _import_module("build_deck")
verify_korean = _import_module("verify_korean")


pytestmark = pytest.mark.design_team


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


def _has_python_pptx() -> bool:
    return importlib.util.find_spec("pptx") is not None


def _sample_vars(layout: str, suffix: str = "") -> dict[str, str]:
    """layout별 샘플 변수 dict (한글 가득)."""
    base: dict[str, dict[str, str]] = {
        "executive": {
            "title": f"임원 보고{suffix}",
            "eyebrow": "분기 실적",
            "kpi_headline": "매출 320억 달성",
            "lede": "전년 동기 대비 24% 성장하며 사상 최대 실적을 기록했습니다.",
            "bullet_1": "신규 고객 12만명 확보",
            "bullet_2": "구독 갱신율 92%",
            "bullet_3": "운영 비용 8% 절감",
            "author": "전략기획실",
            "date": "2026년 5월",
            "page_number": "01",
        },
        "pitch": {
            "title": f"피치 덱{suffix}",
            "badge": "보험 혁신",
            "headline": "보험을 다시 쓰다",
            "subcopy": "데이터 기반 맞춤 보험으로 한국인 1만 가구의 보험료를 절감했습니다.",
            "cta_label": "다음 라운드 목표",
            "cta_value": "100억 원 시리즈 A",
        },
        "retro": {
            "title": f"4월 회고{suffix}",
            "eyebrow": "월간 회고",
            "keep_label": "잘한 점",
            "keep_1": "스프린트 일정 준수율 100%",
            "keep_2": "팀 간 커뮤니케이션 개선",
            "keep_3": "버그 발생률 30% 감소",
            "problem_label": "문제점",
            "problem_1": "테스트 자동화 커버리지 부족",
            "problem_2": "온콜 부담 특정 인원 집중",
            "problem_3": "고객 응대 응답 시간 지연",
            "try_label": "시도할 점",
            "try_1": "TDD 도입 파일럿",
            "try_2": "온콜 로테이션 균등화",
            "try_3": "고객 응대 SLA 30분 목표",
        },
        "status": {
            "title": f"주간 운영 현황{suffix}",
            "period": "2026년 5월 1주차",
            "kpi1_label": "신규 가입",
            "kpi1_value": "1240",
            "kpi1_delta": "전주 대비 12 퍼센트 증가",
            "kpi2_label": "활성 사용자",
            "kpi2_value": "48300",
            "kpi2_delta": "전주 대비 5 퍼센트 증가",
            "kpi3_label": "매출",
            "kpi3_value": "8.2억 원",
            "kpi3_delta": "전주 대비 18 퍼센트 증가",
            "kpi4_label": "이탈률",
            "kpi4_value": "2.1",
            "kpi4_delta": "전주 대비 0.4 포인트 감소",
            "comment": "신규 캠페인 효과로 가입자가 늘었고 이탈률은 안정화 추세입니다.",
        },
        "plan": {
            "title": f"하반기 로드맵{suffix}",
            "eyebrow": "프로젝트 일정",
            "lede": "단계별 마일스톤과 책임자를 정리했습니다.",
            "when_1": "7월",
            "what_1": "베타 기능 출시 및 초기 사용자 모집",
            "when_2": "8월",
            "what_2": "결제 시스템 통합 및 가격 정책 확정",
            "when_3": "9월",
            "what_3": "정식 런칭 및 마케팅 캠페인 가동",
            "when_4": "10월",
            "what_4": "리텐션 분석 및 다음 분기 계획 수립",
        },
        "intro": {
            "title": f"보험 혁신 보고서{suffix}",
            "subtitle": "2026년 상반기 사업 전략",
            "eyebrow": "사내 발표",
            "presenter": "전략기획실 김지훈",
            "date": "2026년 5월 3일",
        },
        "agenda": {
            "title": f"오늘 다룰 내용{suffix}",
            "eyebrow": "목차",
            "lede": "다섯 가지 핵심 주제로 구성했습니다.",
            "item_1": "현황과 시장 분석",
            "item_2": "핵심 전략 방향",
            "item_3": "주요 실행 과제",
            "item_4": "성과 측정 지표",
            "item_5": "질의응답 및 토론",
        },
        "content": {
            "title": f"신규 시장 진입 전략{suffix}",
            "eyebrow": "전략 방향",
            "lede": "30대 직장인을 핵심 타겟으로 모바일 전용 상품을 출시합니다.",
            "bullet_1": "월 만원대 마이크로 보험 라인업 출시",
            "bullet_2": "카카오 친구톡 기반 고객 응대 자동화",
            "bullet_3": "직장인 커뮤니티와 제휴 마케팅 진행",
            "visual_label": "타겟 고객",
            "visual_value": "320만",
            "visual_caption": "30대 직장인 잠재 고객 규모",
        },
        "summary": {
            "title": f"정리하며{suffix}",
            "eyebrow": "요약",
            "point1_title": "데이터 기반 의사결정",
            "point1_desc": "주간 단위 KPI 검토를 정례화하여 빠르게 대응합니다.",
            "point2_title": "고객 경험 우선",
            "point2_desc": "응대 시간을 30분 이내로 단축하고 만족도를 측정합니다.",
            "point3_title": "팀 단위 실행",
            "point3_desc": "월간 회고를 통해 조직 학습을 누적합니다.",
            "action_label": "다음 액션",
            "action_copy": "다음 주까지 실행 계획서를 정리해 공유합니다.",
        },
        "qa": {
            "title": f"질의응답 세션{suffix}",
            "headline": "질문해 주세요",
            "subcopy": "오늘 발표에 대한 의견과 추가 논의가 필요한 부분을 자유롭게 말씀해 주세요.",
            "eyebrow": "마무리",
            "contact_label": "연락처",
            "contact_name": "전략기획실 김지훈",
            "contact_email": "jihoon@example.com",
        },
    }
    return base[layout]


# --- 1. linear 스타일 PPT 10장 (= 매거진 5종 + 덱 5종) -----------------------


def test_scenario_1_linear_style_ppt_10_slides(tmp_path: Path) -> None:
    """1. linear 스타일 PPT 10장 — 매거진 5 + 덱 5 = 10장 빌드."""
    layout_names = [
        "executive", "pitch", "retro", "status", "plan",
        "intro", "agenda", "content", "summary", "qa",
    ]
    variables_list = [_sample_vars(name) for name in layout_names]

    # linear brand는 design-md에 없을 수 있음 → fallback로 빌드되어야 함
    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="linear",
        output_dir=tmp_path / "linear",
    )

    assert len(result.html_paths) == 10
    for p in result.html_paths:
        assert p.exists(), f"slide html not generated: {p}"
        assert p.stat().st_size > 100, f"slide html too small: {p}"
    assert result.manifest_path.exists()


# --- 2. supabase 스타일 덱 7장 (덱 5 + 매거진 2) -----------------------------


def test_scenario_2_supabase_style_deck_7_slides(tmp_path: Path) -> None:
    """2. supabase 스타일 덱 7장 — 덱 5 + 매거진 2."""
    layout_names = [
        "intro", "agenda", "content", "summary", "qa",
        "executive", "status",
    ]
    variables_list = [_sample_vars(name) for name in layout_names]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="supabase",
        output_dir=tmp_path / "supabase",
    )
    assert len(result.html_paths) == 7
    # supabase는 design-md에 존재함 → tokens가 fallback과 다를 수 있음 (단언 안 함, 빌드만 OK)


# --- 3. apple 미니멀 덱 3종 ------------------------------------------------


def test_scenario_3_apple_minimal_deck_3_slides(tmp_path: Path) -> None:
    """3. apple 미니멀 덱 3종 — 디자인 토큰 fallback 또는 design-md 사용."""
    layout_names = ["intro", "content", "qa"]
    variables_list = [_sample_vars(name) for name in layout_names]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="apple",
        output_dir=tmp_path / "apple",
    )
    assert len(result.html_paths) == 3


# --- 4. 한글 100% string-match -------------------------------------------


def test_scenario_4_korean_100_percent_match(tmp_path: Path) -> None:
    """4. 한글 100% — manifest의 모든 한글이 HTML에 string-match로 존재."""
    layout_names = ["executive", "pitch", "retro", "intro", "qa"]
    variables_list = [_sample_vars(name) for name in layout_names]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="supabase",
        output_dir=tmp_path / "korean",
    )

    report = verify_korean.verify_html(result.manifest_path, result.output_dir)
    assert report["pass"] is True, f"korean string-match failed: {report}"
    for slide in report["results"]:
        assert slide["pass"], f"slide {slide['file']} missing: {slide.get('missing')}"


# --- 5. PPTX export -------------------------------------------------------


def test_scenario_5_pptx_export(tmp_path: Path) -> None:
    """5. PPTX export — html_to_pptx 호출 후 .pptx 파일 존재 + 슬라이드 수 일치."""
    if not _has_python_pptx():
        pytest.skip("python-pptx not installed")
    html_to_pptx = _import_module("html_to_pptx")

    layout_names = ["executive", "intro", "qa"]
    variables_list = [_sample_vars(name) for name in layout_names]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="supabase",
        output_dir=tmp_path / "pptx",
    )
    pptx_out = tmp_path / "pptx" / "deck.pptx"
    pptx_path = html_to_pptx.compile_pptx(
        result.output_dir, result.manifest_path, pptx_out
    )
    assert pptx_path.exists()
    assert pptx_path.stat().st_size > 1024

    import pptx as _pptx  # type: ignore[import-not-found]

    presentation = _pptx.Presentation(str(pptx_path))
    assert len(presentation.slides) == 3


# --- 6. PPTX 텍스트 한글 검증 -------------------------------------------


def test_scenario_6_pptx_korean_extraction(tmp_path: Path) -> None:
    """6. PPTX 텍스트 검증 — python-pptx로 다시 읽어 한글 string-match."""
    if not _has_python_pptx():
        pytest.skip("python-pptx not installed")
    html_to_pptx = _import_module("html_to_pptx")

    layout_names = ["pitch", "agenda"]
    variables_list = [_sample_vars(name) for name in layout_names]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand=None,  # fallback tokens
        output_dir=tmp_path / "pptx2",
    )
    pptx_out = tmp_path / "pptx2" / "deck.pptx"
    html_to_pptx.compile_pptx(result.output_dir, result.manifest_path, pptx_out)

    report = verify_korean.verify_pptx_text(result.manifest_path, pptx_out)
    assert report["pass"] is True, f"pptx korean missing: {report}"
    assert report["slide_count"] == 2


# --- 7. 외부 API SDK import 0건 ----------------------------------------


def test_scenario_7_no_external_api_sdk_imports() -> None:
    """7. 외부 API mock 차단 — openai/anthropic/google.generativeai import 0건."""
    forbidden_modules = ("openai", "anthropic", "google.generativeai", "genai")
    py_files = list(SCRIPTS_DIR.rglob("*.py"))
    assert py_files, "scripts/*.py not found"

    offenders: list[tuple[str, str]] = []
    for path in py_files:
        text = path.read_text(encoding="utf-8")
        for mod in forbidden_modules:
            # import openai / from openai import ...
            patterns = [
                rf"^\s*import\s+{re.escape(mod)}(\s|$|\.)",
                rf"^\s*from\s+{re.escape(mod)}(\.|\s+import\s+)",
            ]
            for pat in patterns:
                if re.search(pat, text, re.MULTILINE):
                    offenders.append((str(path), mod))

    assert not offenders, f"forbidden imports found: {offenders}"


# --- 8. 직접 URL 사용 0건 ---------------------------------------------


def test_scenario_8_no_external_api_urls() -> None:
    """8. 직접 URL 차단 — api.openai.com / api.anthropic.com / generativelanguage 0건."""
    forbidden_urls = (
        "api.openai.com",
        "api.anthropic.com",
        "generativelanguage.googleapis.com",
    )
    py_files = list(SCRIPTS_DIR.rglob("*.py"))
    offenders: list[tuple[str, str]] = []
    for path in py_files:
        text = path.read_text(encoding="utf-8")
        for url in forbidden_urls:
            if url in text:
                offenders.append((str(path), url))
    assert not offenders, f"forbidden URLs found: {offenders}"


# --- 9. 폰트 fallback 차단 검증 ---------------------------------------


def test_scenario_9_font_stack_pretendard_first(tmp_path: Path) -> None:
    """9. 폰트 fallback 차단 — 모든 슬라이드 HTML body의 font-family가 Pretendard 시작."""
    layout_names = build_deck.list_layouts()
    variables_list = [_sample_vars(name) for name in layout_names]
    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="apple",
        output_dir=tmp_path / "font",
    )
    report = verify_korean.verify_font_stack_in_html(result.output_dir)
    assert report["pass"] is True, f"font stack check failed: {report}"


# --- 10. design-md inject 검증 ----------------------------------------


def test_scenario_10_design_md_inject(tmp_path: Path) -> None:
    """10. design-md inject — 디자인 토큰이 HTML/CSS에 반영되는지."""
    layout_names = ["executive"]
    variables_list = [_sample_vars("executive")]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="supabase",
        output_dir=tmp_path / "tokens",
    )
    assert "primary" in result.tokens
    html = result.html_paths[0].read_text(encoding="utf-8")
    # 디자인 토큰이 CSS 변수 자리에 들어갔는지 (--color-primary 라인에 hex 색상이 있는지)
    assert "--color-primary:" in html
    # 한글 폰트 스택은 항상 존재
    assert "Pretendard" in html
    assert "Noto Sans KR" in html


# --- 11. fallback 토큰 -----------------------------------------------------


def test_scenario_11_fallback_tokens_invalid_brand(tmp_path: Path) -> None:
    """11. fallback 토큰 — 잘못된 brand 입력 시 fallback_tokens()로 정상 빌드."""
    layout_names = ["intro"]
    variables_list = [_sample_vars("intro")]

    result = build_deck.build(
        layout_names=layout_names,
        variables_list=variables_list,
        brand="this-brand-definitely-does-not-exist",
        output_dir=tmp_path / "fallback",
    )
    assert result.html_paths[0].exists()
    # fallback에서는 Pretendard 폰트가 보장됨
    html = result.html_paths[0].read_text(encoding="utf-8")
    assert "Pretendard" in html


# --- 12. registry.json 무결성 ----------------------------------------


def test_scenario_12_registry_integrity() -> None:
    """12. registry.json 무결성 — 10종 모두 존재 + vars 스키마 OK."""
    registry_path = TEMPLATES_DIR / "registry.json"
    assert registry_path.exists()
    with registry_path.open("r", encoding="utf-8") as f:
        registry = json.load(f)
    layouts = registry.get("layouts", {})
    expected = {
        "executive", "pitch", "retro", "status", "plan",
        "intro", "agenda", "content", "summary", "qa",
    }
    assert set(layouts.keys()) == expected, f"missing/extra layouts: {set(layouts.keys()) ^ expected}"

    # 각 layout의 schema(*.json) 파일 검증
    for name, meta in layouts.items():
        schema_path = TEMPLATES_DIR / meta["schema"]
        assert schema_path.exists(), f"schema missing for {name}: {schema_path}"
        with schema_path.open("r", encoding="utf-8") as f:
            schema = json.load(f)
        assert "vars" in schema, f"vars missing in schema {name}"
        assert "korean_slots" in schema, f"korean_slots missing in {name}"
        # 한글 슬롯이 vars에 모두 존재
        for slot in schema["korean_slots"]:
            assert slot in schema["vars"], f"slot {slot} not in vars for {name}"


# --- 추가: 스킬 메타 무결성 -------------------------------------------


def test_skill_md_frontmatter_present() -> None:
    """SKILL.md에 name/description frontmatter가 있는지."""
    skill_md = SKILL_ROOT / "SKILL.md"
    assert skill_md.exists()
    text = skill_md.read_text(encoding="utf-8")
    assert text.startswith("---")
    assert "name: magazine-ppt-ko" in text
    assert "description:" in text


def test_no_http_client_imports() -> None:
    """urlopen / requests / httpx import 0건 (IDS §0.5 보강 검사)."""
    forbidden = ("urllib.request", "requests", "httpx", "aiohttp")
    py_files = list(SCRIPTS_DIR.rglob("*.py"))
    offenders: list[tuple[str, str]] = []
    for path in py_files:
        text = path.read_text(encoding="utf-8")
        for mod in forbidden:
            if re.search(rf"^\s*import\s+{re.escape(mod)}", text, re.MULTILINE):
                offenders.append((str(path), mod))
            if re.search(rf"^\s*from\s+{re.escape(mod)}\s+import", text, re.MULTILINE):
                offenders.append((str(path), mod))
    assert not offenders, f"http client imports found: {offenders}"
