"""tests/dispatch/test_routing_classification.py — task-2473 regression test.

dispatch.py 휴리스틱 false-positive hotfix 검증.

5건 시나리오:
1. 백틱 코드 블록 안의 디자인 키워드 → coding (false positive 없음)
2. regex pattern 안의 키워드 → coding
3. 코딩 컨텍스트 + 키워드 (Gemini gate / severity badge) → coding
4. 실제 디자인 작업 spec → design (true positive 보존)
5. 정상 코딩 작업 spec → coding (정상 동작 보존)
"""
from __future__ import annotations
import sys
from pathlib import Path

# /home/jay/workspace를 sys.path에 추가
WORKSPACE = Path(__file__).resolve().parents[2]
if str(WORKSPACE) not in sys.path:
    sys.path.insert(0, str(WORKSPACE))

from utils.dispatch_routing import classify_task_routing  # noqa: E402


def test_backtick_code_block_design_keywords_classified_as_coding():
    """백틱 코드 블록 안의 디자인 키워드는 coding 분류."""
    task_desc = """task-X: hardening 작업.

    아래 패턴을 탐지해야 합니다 (코드 검증 대상):
    `image markdown severity badge` 형식의 표현
    `![high](...)` `![critical](...)` 등
    """
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding", (
        f"백틱 코드 블록 안의 디자인 키워드는 coding이어야 함. matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )
    assert d.result == "allow"


def test_regex_pattern_design_keywords_classified_as_coding():
    """regex pattern 안의 키워드는 coding 분류."""
    task_desc = r"""task-Y: regex pattern 휴리스틱 정정.

    다음 정규식 표현을 탐지: !\[(high|critical)\] 형식.
    raw string r"image|banner|design" 패턴.
    """
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding", (
        f"regex pattern 안의 키워드는 coding이어야 함. matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )
    assert d.result == "allow"


def test_coding_context_signals_override_design_keywords():
    """코딩 컨텍스트 시그널 다수 + 디자인 키워드 → coding 분류."""
    task_desc = """task-Z: Gemini gate severity badge 검증 hardening.

    regression test로 false positive 차단.
    qc-gate 룰 정정. 검사 대상 탐지 휴리스틱 보강.
    image 키워드도 본문에 등장하지만 코드 블록과 동등한 검증 컨텍스트.
    """
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding", (
        f"코딩 컨텍스트 시그널이 design을 override 해야 함. matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )
    assert d.result == "allow"
    assert len(d.context_signals) >= 2, f"컨텍스트 시그널 검출 누락: {d.context_signals}"


def test_real_design_task_spec_classified_as_design():
    """실제 디자인 작업 spec → design 분류 (true positive 보존)."""
    task_desc = """task-A: InsuRo 신규 광고 배너 디자인 제작.

    1080x1080 SNS 광고 이미지 5종.
    포스터 디자인 가이드라인 준수.
    배너 일러스트 색상 시안 검토.
    """
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "design", (
        f"실제 디자인 작업은 design이어야 함. matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )
    assert d.result == "block"
    assert len(d.matched_keywords) >= 2, f"디자인 키워드 검출 누락: {d.matched_keywords}"


def test_normal_coding_task_classified_as_coding():
    """정상 코딩 작업 spec → coding 분류 (정상 동작 보존)."""
    task_desc = """task-B: API endpoint 추가 + DB 마이그레이션.

    /api/users 엔드포인트에 POST/PUT 추가.
    Postgres 스키마 변경 (users 테이블 컬럼 추가).
    pytest 단위 테스트 5건.
    """
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding"
    assert d.result == "allow"
    assert d.matched_keywords == [], f"정상 코딩 작업에 디자인 키워드 매칭되면 안 됨: {d.matched_keywords}"


def test_audit_jsonl_appends_entry(tmp_path, monkeypatch):
    """audit jsonl이 정상 append 되는지 확인."""
    fake_audit = tmp_path / "dispatch-routing-decision.jsonl"
    monkeypatch.setattr("utils.dispatch_routing.AUDIT_PATH", fake_audit)
    classify_task_routing("새 광고 배너 디자인 제작", task_id="test-1", task_file="test.md", write_audit=True)
    classify_task_routing("API 엔드포인트 추가", task_id="test-2", task_file="test.md", write_audit=True)
    assert fake_audit.exists()
    lines = fake_audit.read_text(encoding="utf-8").strip().split("\n")
    assert len(lines) == 2
    import json
    e1 = json.loads(lines[0])
    e2 = json.loads(lines[1])
    assert e1["task_id"] == "test-1"
    assert e1["classification"] == "design"
    assert e1["result"] == "block"
    assert e2["task_id"] == "test-2"
    assert e2["classification"] == "coding"
    assert e2["result"] == "allow"
    # 필드 schema 확인
    for required in ("ts", "task_id", "task_file", "matched_keywords", "context_signals", "classification", "result", "score", "raw_text_hash"):
        assert required in e1, f"audit entry missing field: {required}"


def test_word_boundary_no_false_positive():
    """imagery, imagine 같은 영어 합성어는 매칭 안 됨."""
    task_desc = "task-C: imagine a new feature with imagery rendering. designation system upgrade."
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding", (
        f"imagery/imagine/designation은 단어 경계로 분리되어야 함. matched={d.matched_keywords}"
    )
    assert "image" not in d.matched_keywords
    assert "design" not in d.matched_keywords


def test_task_2472_spec_simulation_no_false_positive():
    """task-2472 spec 본문 시뮬레이션 — false positive 차단 검증."""
    task_2472_path = WORKSPACE / "memory" / "tasks" / "task-2472.md"
    if not task_2472_path.exists():
        import pytest
        pytest.skip("task-2472.md 없음")
    task_desc = task_2472_path.read_text(encoding="utf-8")
    d = classify_task_routing(task_desc, task_id="task-2472", task_file=str(task_2472_path), write_audit=False)
    assert d.classification == "coding", (
        f"task-2472는 코드 hardening 작업이므로 coding이어야 함. "
        f"matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )
    assert d.result == "allow"


def test_escaped_quote_in_raw_string_strip():
    """raw string 안에 이스케이프된 따옴표가 있어도 strip이 끊기지 않음 (마아트/Gemini 발견)."""
    task_desc = (
        'task-Q: regex 작업. raw string r"image quote \\" middle quote \\" end" 패턴 처리.\n'
        'hardening regression test 컨텍스트.'
    )
    d = classify_task_routing(task_desc, write_audit=False)
    assert d.classification == "coding", (
        f"이스케이프된 따옴표가 있어도 raw string 전체가 strip되어야 함. "
        f"matched={d.matched_keywords}, score={d.score}"
    )
    assert "image" not in d.matched_keywords or len(d.context_signals) >= 1


def test_unclosed_code_fence_does_not_crash():
    """닫히지 않은 펜스 코드 블록도 분류기가 crash하지 않고 정확히 분류 (마아트/Gemini 발견).

    펜스가 닫히지 않아도 코드 블록 안의 디자인 키워드가 false positive를 만들지 않아야 함.
    """
    task_desc = (
        "task-R: hardening regression test.\n"
        "```python\n"
        "design_keywords = ['디자인']  # 닫히지 않은 펜스 (의도적 edge case)\n"
        "이미지 광고 배너 키워드도 본문에 등장하나 코드 검증 컨텍스트임.\n"
    )
    d = classify_task_routing(task_desc, write_audit=False)
    assert isinstance(d.score, int)
    # 닫히지 않은 펜스도 strip되어야 코딩 컨텍스트로 분류됨
    assert d.classification == "coding", (
        f"닫히지 않은 펜스 안의 키워드는 coding이어야 함. "
        f"matched={d.matched_keywords}, signals={d.context_signals}, score={d.score}"
    )


def test_dispatch_cli_dry_run_task_2472_no_qc_gate():
    """CLI 통합: dispatch.py --task-file task-2472.md --dry-run → qc-gate 미트리거."""
    import subprocess, json as _json, os
    repo = WORKSPACE
    task_2472 = repo / "memory" / "tasks" / "task-2472.md"
    if not task_2472.exists():
        import pytest
        pytest.skip("task-2472.md 없음")
    result = subprocess.run(
        ["python3", "dispatch.py", "--task-file", str(task_2472), "--dry-run"],
        cwd=str(repo), capture_output=True, text=True, timeout=30,
        env={**os.environ, "PYTHONPATH": str(repo)},
    )
    assert result.returncode == 0, f"dry-run 실패: stderr={result.stderr}"
    payload = _json.loads(result.stdout.strip().splitlines()[-1])
    assert payload["status"] == "ok"
    assert payload["dry_run"] is True
    assert payload["classification"] == "coding", f"task-2472는 coding이어야 함: {payload}"
    assert payload["qc_gate_triggered"] is False


def test_dispatch_cli_dry_run_design_task_blocks():
    """CLI 통합: 실제 디자인 spec dry-run → qc-gate 트리거 (true positive 보존)."""
    import subprocess, json as _json, os, tempfile
    repo = WORKSPACE
    spec = (
        "# task-design-test: InsuRo 광고 배너 디자인 제작\n\n"
        "- 1080x1080 SNS 광고 이미지 5종\n"
        "- 포스터 디자인 + 일러스트 색상 시안\n"
        "- 배너 시안 검토\n"
    )
    with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False, encoding="utf-8") as f:
        f.write(spec)
        spec_path = f.name
    try:
        result = subprocess.run(
            ["python3", "dispatch.py", "--task-file", spec_path, "--dry-run"],
            cwd=str(repo), capture_output=True, text=True, timeout=30,
            env={**os.environ, "PYTHONPATH": str(repo)},
        )
        assert result.returncode == 0, f"dry-run 실패: stderr={result.stderr}"
        payload = _json.loads(result.stdout.strip().splitlines()[-1])
        assert payload["classification"] == "design"
        assert payload["result"] == "block"
        assert payload["qc_gate_triggered"] is True
    finally:
        import os as _os
        _os.unlink(spec_path)
