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

검증 목표:
- spec §3 dry-run 기본값 검증
- FINISH_TASK_STASH_APPROVE 미설정 시 stash 변경 없음
- spec §2 결정 흐름 참조구현(reference implementation)으로 dry-run 결정 검증
- audit log 포맷 검증 (approval_mode == "dry-run", action == "dry-run-pop")

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

import json
import os
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
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)


def _stash_list(repo_dir: str) -> list[str]:
    """git stash list 반환."""
    result = subprocess.run(
        ["git", "stash", "list"],
        cwd=repo_dir,
        capture_output=True,
        text=True,
        env=_git_env(),
    )
    if result.returncode != 0 or not result.stdout.strip():
        return []
    return result.stdout.strip().splitlines()


# ---------------------------------------------------------------------------
# spec §2 정책 결정 reference implementation
# ---------------------------------------------------------------------------

def decide_action(
    source: str,
    approve: bool,
    pr_verified: bool,
    idx_in_drop_list: bool,
    etid: str | None,
    task_id: str | None,
) -> str:
    """
    spec §2 결정 흐름 reference implementation.

    Returns:
        action 문자열:
            "dry-run-pop"   — APPROVE 미설정, pre-task
            "dry-run-drop"  — APPROVE 미설정, other-files
            "popped"        — APPROVE=1, pre-task
            "dropped"       — APPROVE=1, other-files + idx_in_drop_list
            "preserved"     — wip / quarantine / unknown, 또는 조건 미충족 finish-task
            "skipped"       — 알 수 없는 source
    """
    if source == "pre-task":
        if approve:
            return "popped"
        return "dry-run-pop"

    if source == "finish-task":
        if approve and pr_verified and (etid == task_id):
            return "popped"
        if not approve:
            return "dry-run-pop"
        # approve=1 이지만 pr_verified 또는 task_id 불일치
        return "preserved"

    if source == "other-files":
        if approve and idx_in_drop_list:
            return "dropped"
        if not approve:
            return "dry-run-drop"
        # approve=1 이지만 idx_in_drop_list 없음
        return "dry-run-drop"

    if source in ("wip", "quarantine", "unknown"):
        return "preserved"

    return "skipped"


def simulate_dispatch(entries: list[dict], approve: bool, task_id: str) -> dict:
    """
    spec §2/§3 기반 dispatch 시뮬레이션.
    audit log 포맷(§4.2)에 맞는 dict 반환.
    """
    decisions = []
    skipped_unknown_count = 0

    for e in entries:
        source = e["source"]
        etid = e.get("task_id")
        idx = e["index"]

        action = decide_action(
            source=source,
            approve=approve,
            pr_verified=False,   # test 환경 — PR 머지 없음
            idx_in_drop_list=False,
            etid=etid,
            task_id=task_id,
        )

        if source == "unknown":
            skipped_unknown_count += 1

        decisions.append({
            "index": idx,
            "source": source,
            "task_id": etid,
            "reason": e.get("reason", ""),
            "action": action,
        })

    return {
        "timestamp_utc": datetime.now(timezone.utc).isoformat(),
        "task_id": task_id,
        "approval_mode": "approved" if approve else "dry-run",
        "stash_count_before": len(entries),
        "decisions": decisions,
        "skipped_unknown_count": skipped_unknown_count,
    }


# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------

@pytest.fixture()
def pretask_repo():
    """pre-task stash 1건만 시드된 임시 repo."""
    repo_dir = _init_temp_repo()
    _stash_push(repo_dir, "WIP: pre-task-2571 stash sample", "b.txt")
    yield repo_dir
    shutil.rmtree(repo_dir, ignore_errors=True)


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

def test_dryrun_stash_not_modified(pretask_repo):
    """
    dry-run 시뮬레이션 후 stash 수가 변하지 않아야 한다.
    (실제 pop/drop 없음 검증)
    """
    before = _stash_list(pretask_repo)
    assert len(before) == 1, f"시드 후 stash 수가 1이 아님: {before}"

    # stash_audit.py 는 read-only 이므로 stash 수 변화 없음
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    assert len(entries) == 1

    after = _stash_list(pretask_repo)
    assert len(after) == 1, (
        f"dry-run 시뮬레이션 후 stash 수가 변경됨: before={len(before)}, after={len(after)}"
    )


def test_dryrun_decision_pretask_is_dry_run_pop(pretask_repo):
    """
    APPROVE 미설정 상태에서 pre-task stash 의 action 은 dry-run-pop 이어야 한다.
    """
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    assert len(entries) == 1

    log = simulate_dispatch(entries, approve=False, task_id="task-2571")

    assert log["approval_mode"] == "dry-run", (
        f"approval_mode 기대: dry-run, 실제: {log['approval_mode']}"
    )
    assert len(log["decisions"]) == 1
    decision = log["decisions"][0]
    assert decision["action"] == "dry-run-pop", (
        f"action 기대: dry-run-pop, 실제: {decision['action']}"
    )


def test_dryrun_audit_log_has_required_fields(pretask_repo):
    """
    시뮬레이션 audit log 가 spec §4.2 필수 필드를 포함해야 한다.
    """
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    log = simulate_dispatch(entries, approve=False, task_id="task-2571")

    required_fields = ["timestamp_utc", "task_id", "approval_mode",
                       "stash_count_before", "decisions", "skipped_unknown_count"]
    for field in required_fields:
        assert field in log, (
            f"audit log 에 '{field}' 필드 없음\nlog keys: {list(log.keys())}"
        )


def test_dryrun_audit_log_timestamp_is_utc_aware(pretask_repo):
    """
    timestamp_utc 가 UTC timezone-aware ISO 형식이어야 한다 (spec §4.3).
    """
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    log = simulate_dispatch(entries, approve=False, task_id="task-2571")

    ts_str = log["timestamp_utc"]
    # isoformat() with timezone 은 +00:00 포함 또는 Z suffix 중 하나
    assert ("+00:00" in ts_str or "Z" in ts_str or "UTC" in ts_str), (
        f"timestamp_utc 가 timezone-aware 가 아닌 것으로 보임: {ts_str}"
    )


def test_dryrun_decision_source_matches_entry(pretask_repo):
    """decisions[0].source 가 entries[0].source 와 일치해야 한다."""
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    log = simulate_dispatch(entries, approve=False, task_id="task-2571")

    assert log["decisions"][0]["source"] == entries[0]["source"], (
        f"decision source ({log['decisions'][0]['source']}) != "
        f"entry source ({entries[0]['source']})"
    )


def test_dryrun_approve_flag_changes_action_to_popped(pretask_repo):
    """
    APPROVE=1 로 설정 시 pre-task action 이 'popped' 으로 변경되어야 한다
    (reference implementation 검증).
    """
    data = _run_audit_json(pretask_repo)
    entries = data.get("entries", [])
    log = simulate_dispatch(entries, approve=True, task_id="task-2571")

    assert log["approval_mode"] == "approved"
    assert log["decisions"][0]["action"] == "popped", (
        f"APPROVE=1 시 action 기대: popped, 실제: {log['decisions'][0]['action']}"
    )
