"""
tests/regression/test_canonical_workspace_resolver_2517.py

헤임달 (QA 테스터) — task-2517 회귀 테스트 14건
canonical_workspace_resolver 4 ambiguity 구조적 제거 + 정상 흐름 + edge case + hook 통합
"""
from __future__ import annotations

import json
import subprocess
import sys
from pathlib import Path

import pytest

# worktree root → sys.path (기존 regression 테스트 패턴 준수)
_WORKTREE_ROOT = Path(__file__).resolve().parent.parent.parent
if str(_WORKTREE_ROOT) in sys.path:
    sys.path.remove(str(_WORKTREE_ROOT))
sys.path.insert(0, str(_WORKTREE_ROOT))

from utils.canonical_workspace_resolver import (  # noqa: E402
    CanonicalWorkspace,
    assert_cwd_matches_workspace,
    assert_finish_task_context,
    assert_main_fresh,
    evaluate_scope_dirty,
    from_dict,
    resolve_canonical_workspace,
    resolve_for_hooks,
    to_json,
)

# ---------------------------------------------------------------------------
# Mock runner 헬퍼
# ---------------------------------------------------------------------------

FAKE_SHA = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
TASK_ID = "task-2517"
BRANCH = "task/task-2517-dev2"


def make_mock_runner(responses: dict):
    """
    responses key: tuple of args
    value: (returncode, stdout, stderr)
    """
    def runner(args, *, cwd=None):
        key = tuple(args)
        if key not in responses:
            raise KeyError(f"Mock missing: {key!r} (cwd={cwd})")
        rc, out, err = responses[key]
        return subprocess.CompletedProcess(args=args, returncode=rc, stdout=out, stderr=err)
    return runner


def _porcelain_worktree_output(main_root: str, worktree_path: str, branch: str, sha: str) -> str:
    return (
        f"worktree {main_root}\n"
        f"HEAD {sha}\n"
        f"branch refs/heads/main\n"
        f"\n"
        f"worktree {worktree_path}\n"
        f"HEAD {sha}\n"
        f"branch refs/heads/{branch}\n"
        f"\n"
    )


def _make_standard_responses(main_root: str, worktree_path: str, branch: str = BRANCH, sha: str = FAKE_SHA):
    """공통으로 사용되는 mock runner 응답 맵을 반환한다.

    show-toplevel은 worktree_path를 반환한다 (git worktree 실제 동작).
    workspace_root는 git worktree list --porcelain의 첫 번째 entry(main_root)로 결정된다.
    is_main 판정은 cwd == workspace_root (== main_root) 비교이므로,
    worktree에서 호출하면 is_main=False가 된다.
    """
    porcelain = _porcelain_worktree_output(main_root, worktree_path, branch, sha)
    return {
        ("git", "rev-parse", "--show-toplevel"): (0, f"{worktree_path}\n", ""),
        ("git", "worktree", "list", "--porcelain"): (0, porcelain, ""),
        ("git", "fetch", "origin", "main", "--quiet"): (0, "", ""),
        ("git", "rev-parse", "origin/main"): (0, f"{sha}\n", ""),
        ("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, f"{branch}\n", ""),
        ("git", "rev-parse", "HEAD"): (0, f"{sha}\n", ""),
    }


# ===========================================================================
# 정상 흐름 (4건)
# ===========================================================================

# 1. test_resolve_in_main_repo
def test_resolve_in_main_repo(tmp_path):
    """main repo에서 resolve → workspace_root == cwd, is_main=True"""
    main_root = tmp_path / "main"
    main_root.mkdir(parents=True)

    # main에서는 show-toplevel이 main_root를 반환, worktree 목록에도 main만 있음
    porcelain = (
        f"worktree {main_root}\n"
        f"HEAD {FAKE_SHA}\n"
        f"branch refs/heads/main\n\n"
    )
    responses = {
        ("git", "rev-parse", "--show-toplevel"): (0, f"{main_root}\n", ""),
        ("git", "worktree", "list", "--porcelain"): (0, porcelain, ""),
        ("git", "fetch", "origin", "main", "--quiet"): (0, "", ""),
        ("git", "rev-parse", "origin/main"): (0, f"{FAKE_SHA}\n", ""),
        ("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "main\n", ""),
        ("git", "rev-parse", "HEAD"): (0, f"{FAKE_SHA}\n", ""),
    }
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=main_root, fetch=True, runner=runner)

    assert ws.workspace_root == main_root.resolve()
    assert ws.cwd == main_root.resolve()
    assert ws.is_main is True
    assert ws.task_id == TASK_ID


# 2. test_resolve_in_worktree
def test_resolve_in_worktree(tmp_path):
    """worktree에서 resolve → worktree_path == cwd, branch_name 매칭, is_main=False

    실제 git worktree에서 `git rev-parse --show-toplevel`은 main_root를 반환한다.
    따라서 workspace_root = main_root, cwd = worktree → is_main=False.
    """
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    porcelain = _porcelain_worktree_output(str(main_root), str(worktree), BRANCH, FAKE_SHA)
    # show-toplevel → main_root (실제 git worktree 동작)
    responses = {
        ("git", "rev-parse", "--show-toplevel"): (0, f"{main_root}\n", ""),
        ("git", "worktree", "list", "--porcelain"): (0, porcelain, ""),
        ("git", "fetch", "origin", "main", "--quiet"): (0, "", ""),
        ("git", "rev-parse", "origin/main"): (0, f"{FAKE_SHA}\n", ""),
        ("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, f"{BRANCH}\n", ""),
        ("git", "rev-parse", "HEAD"): (0, f"{FAKE_SHA}\n", ""),
    }
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    assert ws.worktree_path == worktree.resolve()
    assert ws.cwd == worktree.resolve()
    assert ws.is_main is False
    assert ws.branch_name == BRANCH


# 3. test_main_head_sha_lock
def test_main_head_sha_lock(tmp_path):
    """fetch 후 origin/main HEAD 정확 매칭"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    fixed_sha = "deadbeef" * 5  # 40자
    responses = _make_standard_responses(str(main_root), str(worktree), sha=fixed_sha)
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    assert ws.main_head_sha == fixed_sha
    assert ws.base_sha == fixed_sha


# 4. test_env_var_priority_git_wins
def test_env_var_priority_git_wins(tmp_path, monkeypatch):
    """환경변수 WORKSPACE_ROOT 우선순위 — git rev-parse와 충돌 시 git 우선"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    stale_env_root = "/some/completely/wrong/path"
    monkeypatch.setenv("WORKSPACE_ROOT", stale_env_root)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # git 결과가 우선 — stale env 값이 아닌 git worktree list 첫 entry(main_root)
    assert str(ws.workspace_root) != stale_env_root
    # git worktree list 첫 entry가 main_root이므로 workspace_root == main_root.resolve()
    assert ws.workspace_root == main_root.resolve()


# ===========================================================================
# 4 ambiguity 회귀 (4건)
# ===========================================================================

# 5. test_wrong_cwd_raises
def test_wrong_cwd_raises(tmp_path):
    """잘못된 cwd → assert_cwd_matches_workspace RuntimeError('WRONG_CWD: ...')"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    wrong_dir = tmp_path / "some_other_dir"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)
    wrong_dir.mkdir(parents=True)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # cwd를 수동으로 엉뚱한 곳으로 바꾼 CanonicalWorkspace 생성
    ws_wrong = CanonicalWorkspace(
        task_id=ws.task_id,
        workspace_root=ws.workspace_root,
        worktree_path=ws.worktree_path,
        branch_name=ws.branch_name,
        main_head_sha=ws.main_head_sha,
        base_sha=ws.base_sha,
        cwd=wrong_dir.resolve(),   # 엉뚱한 cwd
        is_main=ws.is_main,
        is_clean=ws.is_clean,
    )

    with pytest.raises(RuntimeError, match="WRONG_CWD"):
        assert_cwd_matches_workspace(ws_wrong)


# 6. test_stale_main_raises
def test_stale_main_raises(tmp_path):
    """fetch 후 origin/main 변경 시뮬레이션 → assert_main_fresh FAIL('STALE_MAIN: ...')"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    sha1 = "a" * 40
    sha2 = "b" * 40  # origin/main이 갱신됐을 때의 SHA

    # resolve 시점에는 sha1 반환
    porcelain = _porcelain_worktree_output(str(main_root), str(worktree), BRANCH, sha1)
    call_count = {"n": 0}

    def stateful_runner(args, *, cwd=None):
        key = tuple(args)
        if key == ("git", "rev-parse", "origin/main"):
            call_count["n"] += 1
            sha = sha1 if call_count["n"] == 1 else sha2
            return subprocess.CompletedProcess(args=args, returncode=0, stdout=f"{sha}\n", stderr="")
        fixed = {
            ("git", "rev-parse", "--show-toplevel"): (0, f"{worktree}\n", ""),
            ("git", "worktree", "list", "--porcelain"): (0, porcelain, ""),
            ("git", "fetch", "origin", "main", "--quiet"): (0, "", ""),
            ("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, f"{BRANCH}\n", ""),
            ("git", "rev-parse", "HEAD"): (0, f"{sha1}\n", ""),
        }
        if key not in fixed:
            raise KeyError(f"Mock missing: {key!r} (cwd={cwd})")
        rc, out, err = fixed[key]
        return subprocess.CompletedProcess(args=args, returncode=rc, stdout=out, stderr=err)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=stateful_runner)
    assert ws.main_head_sha == sha1

    # assert_main_fresh: 두 번째 호출이므로 sha2 반환 → 불일치 → STALE_MAIN
    with pytest.raises(RuntimeError, match="STALE_MAIN"):
        assert_main_fresh(ws, runner=stateful_runner)


# 7. test_dirty_workspace_false_detection
def test_dirty_workspace_false_detection(tmp_path):
    """expected_files 외 파일이 dirty → evaluate_scope_dirty IS_CLEAN (False 반환)"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    expected_files = ["utils/canonical_workspace_resolver.py"]
    # expected_files를 git status 인수로 받으면 clean (빈 stdout)
    clean_status_key = tuple(["git", "status", "--porcelain", "--"] + expected_files)

    responses = _make_standard_responses(str(main_root), str(worktree))
    # expected_files만 포함하는 git status → clean (empty)
    responses[clean_status_key] = (0, "", "")
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # expected_files만 평가 → 외부 파일 dirty 영향 없음 → False (clean)
    result = evaluate_scope_dirty(ws, expected_files, runner=runner)
    assert result is False


# 8. test_finish_task_context_mismatch
def test_finish_task_context_mismatch(tmp_path):
    """HEAD SHA 변경 후 finish_target 불일치 → assert_finish_task_context RuntimeError('FINISH_TASK_CONTEXT_MISMATCH: ...')"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    original_head_sha = "c" * 40
    changed_head_sha = "d" * 40  # HEAD가 바뀐 상황

    responses = _make_standard_responses(str(main_root), str(worktree), sha=original_head_sha)
    # assert_finish_task_context에서 git rev-parse HEAD를 worktree에서 호출
    # changed_head_sha를 반환하도록 오버라이드
    responses[("git", "rev-parse", "HEAD")] = (0, f"{changed_head_sha}\n", "")
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # finish_target은 original_head_sha 기준으로 설정
    finish_target = {
        "head_sha": original_head_sha,
        "branch_name": ws.branch_name,
        "worktree_path": str(ws.worktree_path),
    }

    # 현재 HEAD가 changed_head_sha → original과 불일치 → FINISH_TASK_CONTEXT_MISMATCH
    with pytest.raises(RuntimeError, match="FINISH_TASK_CONTEXT_MISMATCH"):
        assert_finish_task_context(ws, finish_target, runner=runner)


# ===========================================================================
# Edge case (3건)
# ===========================================================================

# 9. test_resolve_when_worktree_missing
def test_resolve_when_worktree_missing(tmp_path):
    """worktree 없을 때 resolve — worktree_path만 산출, 생성 X (실제 디렉토리 없어도 OK)"""
    main_root = tmp_path / "main"
    main_root.mkdir(parents=True)

    # worktree list에 task-2517 항목 없음
    porcelain = (
        f"worktree {main_root}\n"
        f"HEAD {FAKE_SHA}\n"
        f"branch refs/heads/main\n\n"
    )
    responses = {
        ("git", "rev-parse", "--show-toplevel"): (0, f"{main_root}\n", ""),
        ("git", "worktree", "list", "--porcelain"): (0, porcelain, ""),
        ("git", "fetch", "origin", "main", "--quiet"): (0, "", ""),
        ("git", "rev-parse", "origin/main"): (0, f"{FAKE_SHA}\n", ""),
        ("git", "rev-parse", "--abbrev-ref", "HEAD"): (0, "main\n", ""),
        ("git", "rev-parse", "HEAD"): (0, f"{FAKE_SHA}\n", ""),
    }
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=main_root, fetch=True, runner=runner)

    # worktree_path는 산출되지만 실제 디렉토리 생성 X
    assert ws.worktree_path is not None
    # glob 후보 없으면 기본 패턴 사용
    assert "task-2517" in str(ws.worktree_path) or "dev1" in str(ws.worktree_path)
    # 실제 경로가 존재하지 않아도 OK
    # (존재 여부를 assert하지 않음)


# 10. test_env_var_project_path_stale
def test_env_var_project_path_stale(tmp_path, monkeypatch):
    """PROJECT_PATH 환경변수 stale 시 git rev-parse 우선"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    stale_project_path = "/old/stale/project/path"
    monkeypatch.setenv("PROJECT_PATH", stale_project_path)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # git 결과가 우선 — stale PROJECT_PATH 무시
    assert str(ws.workspace_root) != stale_project_path
    # workspace_root는 git worktree list 첫 entry(main_root)여야 함
    assert ws.workspace_root == main_root.resolve()


# 11. test_canonical_workspace_json_round_trip
def test_canonical_workspace_json_round_trip(tmp_path):
    """to_json → from_dict 라운드트립 무결성 (모든 필드 동일)"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws_original = resolve_canonical_workspace(TASK_ID, cwd=worktree, fetch=True, runner=runner)

    # 직렬화 → 역직렬화
    json_str = to_json(ws_original)
    parsed = json.loads(json_str)
    ws_restored = from_dict(parsed)

    # 모든 필드 일치 확인
    assert ws_restored.task_id == ws_original.task_id
    assert ws_restored.workspace_root == ws_original.workspace_root
    assert ws_restored.worktree_path == ws_original.worktree_path
    assert ws_restored.branch_name == ws_original.branch_name
    assert ws_restored.main_head_sha == ws_original.main_head_sha
    assert ws_restored.base_sha == ws_original.base_sha
    assert ws_restored.cwd == ws_original.cwd
    assert ws_restored.is_main == ws_original.is_main
    assert ws_restored.is_clean == ws_original.is_clean


# ===========================================================================
# 6 hook 통합 (3건)
# ===========================================================================

# 12. test_hook_scope_guard_shares_workspace
def test_hook_scope_guard_shares_workspace(tmp_path):
    """resolve_for_hooks('scope-guard') 두 번 호출 시 동일 workspace fields (확정성)"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws1 = resolve_for_hooks(TASK_ID, "scope-guard", cwd=worktree, fetch=True, runner=runner)
    ws2 = resolve_for_hooks(TASK_ID, "scope-guard", cwd=worktree, fetch=True, runner=runner)

    # 핵심 필드 일치 (같은 시점, 같은 mock → deterministic)
    assert ws1.workspace_root == ws2.workspace_root
    assert ws1.worktree_path == ws2.worktree_path
    assert ws1.branch_name == ws2.branch_name
    assert ws1.main_head_sha == ws2.main_head_sha


# 13. test_hook_finish_task_shares_workspace
def test_hook_finish_task_shares_workspace(tmp_path):
    """resolve_for_hooks('finish-task') 결과로 assert_finish_task_context PASS (mismatch 0)"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws = resolve_for_hooks(TASK_ID, "finish-task", cwd=worktree, fetch=True, runner=runner)

    # finish_target을 ws 기반으로 구성 → 불일치 없어야 함
    finish_target = {
        "head_sha": FAKE_SHA,  # mock runner의 git rev-parse HEAD 응답과 동일
        "branch_name": ws.branch_name,
        "worktree_path": str(ws.worktree_path),
    }

    # PASS — 예외 없이 통과해야 함
    assert_finish_task_context(ws, finish_target, runner=runner)


# 14. test_hooks_share_main_head_sha
def test_hooks_share_main_head_sha(tmp_path):
    """resolve_for_hooks('smoke')와 resolve_for_hooks('qc')가 동일 main_head_sha"""
    main_root = tmp_path / "main"
    worktree = tmp_path / ".worktrees" / "task-2517-dev2"
    main_root.mkdir(parents=True)
    worktree.mkdir(parents=True)

    responses = _make_standard_responses(str(main_root), str(worktree))
    runner = make_mock_runner(responses)

    ws_smoke = resolve_for_hooks(TASK_ID, "smoke", cwd=worktree, fetch=True, runner=runner)
    ws_qc = resolve_for_hooks(TASK_ID, "qc", cwd=worktree, fetch=True, runner=runner)

    # 같은 mock runner → 동일 origin/main SHA
    assert ws_smoke.main_head_sha == ws_qc.main_head_sha
    assert ws_smoke.main_head_sha == FAKE_SHA
