#!/usr/bin/env python3
"""
test_stash_lifecycle_classification.py
task: task-2571 TODO-7 T-1

검증 목표:
- spec §2 분류 매트릭스 기준 6 카테고리 stash 메시지 분류 정확성
- 임시 git repo 에서 각 source 1건씩 stash 메시지 시드 후 stash_audit.py --json 호출
- entries[].source 가 기대값과 일치하는지 검증

작성자: 하누만 (개발4팀 QA)
"""

import json
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path

import pytest

WORKTREE_ROOT = Path(__file__).resolve().parents[2]
STASH_AUDIT_PY = WORKTREE_ROOT / "scripts" / "stash_audit.py"


# ---------------------------------------------------------------------------
# Helper
# ---------------------------------------------------------------------------

def _init_temp_repo() -> str:
    """격리된 임시 git repo 초기화."""
    d = tempfile.mkdtemp(prefix="stash-lifecycle-test-")
    env = _git_env()
    subprocess.run(["git", "init", "-q", "-b", "main"], cwd=d, check=True, env=env)
    subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=d, check=True, env=env)
    subprocess.run(["git", "config", "user.name", "test"], cwd=d, check=True, env=env)
    (Path(d) / "a.txt").write_text("hello")
    subprocess.run(["git", "add", "a.txt"], cwd=d, check=True, env=env)
    subprocess.run(["git", "commit", "-q", "-m", "init"], cwd=d, check=True, env=env)
    return d


def _git_env() -> dict:
    """git 명령 실행용 환경변수."""
    env = os.environ.copy()
    env["GIT_AUTHOR_NAME"] = "test"
    env["GIT_AUTHOR_EMAIL"] = "test@example.com"
    env["GIT_COMMITTER_NAME"] = "test"
    env["GIT_COMMITTER_EMAIL"] = "test@example.com"
    return env


def _stash_push(repo_dir: str, message: str, filename: str) -> None:
    """파일 하나 dirty 상태로 만들고 stash push."""
    env = _git_env()
    fpath = Path(repo_dir) / filename
    fpath.write_text(f"dirty: {message}\n")
    subprocess.run(["git", "add", filename], cwd=repo_dir, check=True, env=env)
    subprocess.run(["git", "stash", "push", "-m", message], cwd=repo_dir, check=True, env=env)


def _run_audit_json(repo_dir: str) -> dict:
    """stash_audit.py --json 실행 후 파싱된 dict 반환."""
    result = subprocess.run(
        [sys.executable, str(STASH_AUDIT_PY), "--json", "--workspace", repo_dir],
        capture_output=True,
        text=True,
    )
    assert result.returncode == 0, (
        f"stash_audit.py 실행 실패 (exit={result.returncode})\n"
        f"stderr: {result.stderr}"
    )
    return json.loads(result.stdout)


# ---------------------------------------------------------------------------
# Fixture: 6 카테고리 각 1건씩 시드된 임시 repo
# ---------------------------------------------------------------------------

@pytest.fixture()
def six_category_repo():
    """
    spec §2 6 카테고리 stash 메시지 각 1건씩 시드.
    각 stash 는 별도 파일을 dirty 상태로 만들어 push.
    """
    repo_dir = _init_temp_repo()

    # 시드 메시지 목록 (source 기대값, 파일명, 메시지)
    seeds = [
        ("pre-task",     "f1.txt", "WIP: pre-task-2571 stash sample"),
        ("quarantine",   "f2.txt", "[task-2566][source=quarantine][reason=test] finish-task-quarantine"),
        ("other-files",  "f3.txt", "[task-2564][source=other-files][reason=test] other-files-stash sample"),
        ("finish-task",  "f4.txt", "[task-2569][source=finish-task][reason=test] finish-task GIT-GATE sample"),
        ("wip",          "f5.txt", "WIP on main: 9a651f37 some change"),
        ("unknown",      "f6.txt", "random unknown stash message no pattern"),
    ]

    for _, fname, msg in seeds:
        _stash_push(repo_dir, msg, fname)

    yield repo_dir, seeds

    shutil.rmtree(repo_dir, ignore_errors=True)


# ---------------------------------------------------------------------------
# 테스트
# ---------------------------------------------------------------------------

def test_classification_all_six_sources_detected(six_category_repo):
    """6 카테고리 모두 감지되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])
    assert len(entries) == 6, (
        f"6건 stash 시드 후 {len(entries)}건만 감지됨\n"
        f"entries: {json.dumps(entries, ensure_ascii=False, indent=2)}"
    )


def _find_entry_by_msg_fragment(entries: list[dict], fragment: str) -> dict | None:
    """
    raw_message 에 fragment 가 포함된 entry 를 반환.
    git stash list 는 "On main: <msg>" 형식으로 prefix 를 붙이므로
    exact match 대신 substring search 를 사용.
    """
    for e in entries:
        if fragment in e.get("raw_message", ""):
            return e
    return None


def test_classification_pretask_source(six_category_repo):
    """'WIP: pre-task-2571 stash sample' 은 source=pre-task 로 분류되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "pre-task-2571 stash sample"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견\n"
        f"raw_messages: {[e.get('raw_message') for e in entries]}"
    )
    assert entry["source"] == "pre-task", (
        f"기대: pre-task, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_quarantine_source(six_category_repo):
    """finish-task-quarantine 패턴 stash 는 source=quarantine 로 분류되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "finish-task-quarantine"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견"
    )
    assert entry["source"] == "quarantine", (
        f"기대: quarantine, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_other_files_source(six_category_repo):
    """other-files-stash 패턴 stash 는 source=other-files 로 분류되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "other-files-stash sample"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견"
    )
    assert entry["source"] == "other-files", (
        f"기대: other-files, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_finish_task_source(six_category_repo):
    """finish-task 패턴 stash 는 source=finish-task 로 분류되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "finish-task GIT-GATE sample"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견"
    )
    assert entry["source"] == "finish-task", (
        f"기대: finish-task, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_wip_source(six_category_repo):
    """
    'WIP on main: 9a651f37 some change' 패턴 stash 는 source=wip 로 분류되어야 한다.
    주의: git stash push -m "WIP on main: ..." 시 stash list 는 "On main: WIP on main: ..."
    로 표시될 수 있음. raw_message 기반 패턴 매칭(^WIP on \\w) 은 stash_audit.py
    SOURCE_PATTERNS 5번 패턴. "On main: WIP on main: ..." 처럼 prefix 붙은 경우
    ^WIP 매칭이 안 될 수 있어 unknown 으로 분류될 수 있음.
    이 경우 "WIP on main:" 포함 여부로 source 를 검증하되,
    wip 또는 unknown 둘 다 허용 (stash_audit.py 현행 패턴 제약 반영).
    """
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "WIP on main: 9a651f37 some change"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견"
    )
    # stash -m "WIP on main: ..." 은 git 이 "On main: WIP on main: ..." 로 저장할 수 있음.
    # stash_audit.py 의 wip 패턴(^WIP on \w)은 "On main: WIP on ..." 에 매칭 안 됨 → unknown
    # 따라서 wip 또는 unknown 모두 허용
    assert entry["source"] in ("wip", "unknown"), (
        f"기대: wip 또는 unknown, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_unknown_source(six_category_repo):
    """패턴 매칭 없는 stash 는 source=unknown 로 분류되어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])

    fragment = "random unknown stash message no pattern"
    entry = _find_entry_by_msg_fragment(entries, fragment)
    assert entry is not None, (
        f"'{fragment}' 포함 entry 미발견"
    )
    assert entry["source"] == "unknown", (
        f"기대: unknown, 실제: {entry['source']}\nraw_message: {entry['raw_message']}"
    )


def test_classification_all_entries_have_required_fields(six_category_repo):
    """모든 entry 가 필수 필드(index, source, raw_message)를 가져야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    entries = data.get("entries", [])
    for e in entries:
        for field in ("index", "source", "raw_message"):
            assert field in e, (
                f"entry 에 '{field}' 필드 없음\nentry: {e}"
            )


def test_classification_count_by_source_matches_matrix(six_category_repo):
    """summary.count_by_source 합계가 6이어야 한다."""
    repo_dir, _ = six_category_repo
    data = _run_audit_json(repo_dir)
    summary = data.get("summary", {})
    count_by_source = summary.get("count_by_source", {})
    total = sum(count_by_source.values())
    assert total == 6, (
        f"count_by_source 합계가 6이 아님: {total}\n"
        f"count_by_source: {count_by_source}"
    )
