"""
test_dispatch_counter_fix.py — task-counter 버그 fix (4 layer) 회귀 방지 테스트
task-2380 (dev1/아르고스)

6건 테스트:
1. test_variant_id_filtering               — 변종 ID 필터링
2. test_no_down_correction_in_source       — 다운-보정 elif 블록 제거 검증 (소스 검사)
3. test_counter_up_correction              — 카운터 위로 보정 통합 테스트
4. test_resolve_main_workspace_from_worktree — _resolve_main_workspace() 워크트리 환경
5. test_concurrent_generate_task_id        — flock 동시성 (subprocess 2개 race)
6. test_cleanup_dry_run_lists_without_deleting — cleanup 스크립트 dry-run
"""

import inspect
import json
import subprocess
import sys
import threading
from pathlib import Path

import pytest

# dispatch.py 경로: tests/dev1/conftest.py가 sys.path[0]에 worktree 루트를 삽입함
_WORKTREE_ROOT = Path(__file__).resolve().parents[2]


def _get_dispatch():
    """항상 worktree dispatch.py를 반환 (모듈 캐시 오염 방지)."""
    if "dispatch" in sys.modules:
        m = sys.modules["dispatch"]
        if hasattr(m, "__file__") and m.__file__ and str(_WORKTREE_ROOT) in m.__file__:
            return m
        del sys.modules["dispatch"]
    import dispatch as d  # noqa: PLC0415
    return d


# ─────────────────────────────────────────────────────────────────────────────
# Test 1: 변종 ID 필터링
# ─────────────────────────────────────────────────────────────────────────────

def test_variant_id_filtering(tmp_path):
    """task-9.1, task-900.1 등 변종 ID는 무시하고 4자리 정규 ID만 처리함.

    task-2376 + 1 = 2377 반환, task-2376_2.1은 phase 변형이므로 2376으로 파싱.
    """
    d = _get_dispatch()

    timer_file = tmp_path / "task-timers.json"
    timer_data = {
        "tasks": {
            "task-9.1": {"status": "done"},
            "task-900.1": {"status": "done"},
            "task-2376": {"status": "done"},
            "task-2376_2.1": {"status": "done"},
        }
    }
    timer_file.write_text(json.dumps(timer_data), encoding="utf-8")

    result = d._compute_next_id_from_timers(timer_file)

    assert result == 2377, (
        f"변종 ID(task-9.1, task-900.1) 무시 후 2376+1=2377 이어야 하는데 {result} 반환"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 2: 카운터 신뢰 (다운-보정 없음 — 소스 검사)
# ─────────────────────────────────────────────────────────────────────────────

def test_no_down_correction_in_source():
    """generate_task_id 소스에 다운-보정(elif next_num > timers_max) 블록이 없음을 검증.

    과거 버그: counter=5000, timers_max=2376 → 5000→2376으로 다운 보정하는
    elif 블록이 존재했음. fix 이후 해당 코드가 제거되어야 함.

    xfail: 불칸 작업 전에는 해당 코드가 여전히 존재할 수 있음.
    """
    d = _get_dispatch()
    src = inspect.getsource(d.generate_task_id)

    # 다운-보정 표식 문자열이 없어야 함 (fix 이후 상태)
    has_down_corr_str = "카운터(%d)가 timers 최대(%d) 대비 1000" in src
    has_down_corr_elif = "next_num > timers_max" in src and "next_num - timers_max" in src

    if has_down_corr_str or has_down_corr_elif:
        pytest.xfail(
            "다운-보정 코드가 여전히 존재함 — 불칸 작업(elif 블록 제거) 완료 후 PASS 예상"
        )

    # 이 시점에 도달하면 fix 완료 상태
    assert not has_down_corr_str, "다운-보정 문자열이 여전히 소스에 존재"
    assert not has_down_corr_elif, "next_num > timers_max 하향 보정 분기가 여전히 존재"


# ─────────────────────────────────────────────────────────────────────────────
# Test 3: 카운터 위로 보정 (통합 테스트)
# ─────────────────────────────────────────────────────────────────────────────

def test_counter_up_correction(tmp_path, monkeypatch):
    """counter_file=100, timer_file에 task-2376 → generate_task_id()는 task-2377 반환.

    카운터(100) < timers 최대(2377) → 위로 보정.
    """
    d = _get_dispatch()

    mem_dir = tmp_path / "memory"
    mem_dir.mkdir(parents=True)

    # counter = 100 (timers_max=2377보다 작음)
    counter_file = mem_dir / ".task-counter"
    counter_file.write_text("100", encoding="utf-8")

    # timer_file: task-2376 존재 → _compute_next_id_from_timers 반환 2377
    timer_file = mem_dir / "task-timers.json"
    timer_data = {"tasks": {"task-2376": {"status": "done"}}}
    timer_file.write_text(json.dumps(timer_data), encoding="utf-8")

    # WORKSPACE 및 _resolve_main_workspace 패치
    monkeypatch.setattr(d, "WORKSPACE", tmp_path)
    monkeypatch.setattr(d, "_resolve_main_workspace", lambda: tmp_path)

    result = d.generate_task_id()

    assert result == "task-2377", (
        f"카운터(100) < timers 최대(2377): 위로 보정 후 task-2377이어야 하는데 {result!r} 반환"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 4: _resolve_main_workspace() 워크트리 환경
# ─────────────────────────────────────────────────────────────────────────────

def test_resolve_main_workspace_from_worktree(tmp_path, monkeypatch):
    """git worktree 환경에서 _resolve_main_workspace()가 메인 워크스페이스를 반환하는지 검증."""
    d = _get_dispatch()

    # 1. 메인 git repo 초기화
    main_repo = tmp_path / "main_repo"
    main_repo.mkdir()
    subprocess.run(["git", "init", str(main_repo)], check=True, capture_output=True)
    subprocess.run(
        ["git", "config", "user.email", "test@test.com"],
        cwd=str(main_repo), check=True, capture_output=True,
    )
    subprocess.run(
        ["git", "config", "user.name", "Test"],
        cwd=str(main_repo), check=True, capture_output=True,
    )
    # 커밋 없으면 worktree 불가 → 더미 파일 커밋
    dummy = main_repo / "README.md"
    dummy.write_text("init")
    subprocess.run(["git", "add", "."], cwd=str(main_repo), check=True, capture_output=True)
    subprocess.run(
        ["git", "commit", "-m", "init"],
        cwd=str(main_repo), check=True, capture_output=True,
    )

    # 2. worktree 추가
    wt_path = tmp_path / "worktree1"
    subprocess.run(
        ["git", "worktree", "add", "--detach", str(wt_path)],
        cwd=str(main_repo), check=True, capture_output=True,
    )

    # 3. WORKSPACE를 worktree 경로로 패치
    monkeypatch.setattr(d, "WORKSPACE", wt_path)

    result = d._resolve_main_workspace()

    assert result.resolve() == main_repo.resolve(), (
        f"worktree({wt_path})에서 호출 시 메인 리포({main_repo})를 반환해야 하는데 {result} 반환"
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 5: flock 동시성 (subprocess 2개 spawn)
# ─────────────────────────────────────────────────────────────────────────────

def test_concurrent_generate_task_id(tmp_path):
    """subprocess 2개를 동시에 spawn해서 generate_task_id() 호출.

    두 결과가 달라야 하며(flock 직렬화), 각각 유효한 task-NNNN 형식이어야 함.
    """
    mem_dir = tmp_path / "memory"
    mem_dir.mkdir(parents=True)

    # 초기 카운터
    (mem_dir / ".task-counter").write_text("3000", encoding="utf-8")
    # 빈 timer file
    (mem_dir / "task-timers.json").write_text(
        json.dumps({"tasks": {}}), encoding="utf-8"
    )

    script_file = tmp_path / "concurrent_helper.py"
    script_file.write_text(
        f"""
import sys, json
from pathlib import Path

repo_root = sys.argv[1]
workspace = Path(sys.argv[2])

# worktree dispatch.py 우선 로드
if repo_root in sys.modules.get('dispatch', type('', (), {{'__file__': ''}})).__file__ if 'dispatch' in sys.modules else False:
    pass
else:
    if repo_root not in sys.path:
        sys.path.insert(0, repo_root)
    if 'dispatch' in sys.modules:
        del sys.modules['dispatch']

sys.path.insert(0, repo_root)
import dispatch as d

d.WORKSPACE = workspace
d._resolve_main_workspace = lambda: workspace

result = d.generate_task_id()
print(result)
""",
        encoding="utf-8",
    )

    repo_root = str(_WORKTREE_ROOT)
    results = []
    lock = threading.Lock()

    def run_sub():
        r = subprocess.run(
            [sys.executable, str(script_file), repo_root, str(tmp_path)],
            capture_output=True, text=True, timeout=30,
        )
        out = r.stdout.strip()
        if not out and r.stderr:
            out = f"ERROR: {r.stderr.strip()[:200]}"
        with lock:
            results.append(out)

    threads = [threading.Thread(target=run_sub) for _ in range(2)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()

    assert len(results) == 2, f"subprocess 결과 2개 필요, {len(results)}개 수신"

    id1, id2 = results[0], results[1]

    # 에러 여부 확인
    assert not id1.startswith("ERROR:"), f"프로세스1 에러: {id1}"
    assert not id2.startswith("ERROR:"), f"프로세스2 에러: {id2}"

    # 형식 검증
    import re
    pat = re.compile(r"^task-\d+$")
    assert pat.match(id1), f"결과1 형식 오류: {id1!r}"
    assert pat.match(id2), f"결과2 형식 오류: {id2!r}"

    # 두 ID는 달라야 함 (flock 직렬화)
    assert id1 != id2, (
        f"동시 호출 시 ID가 중복됨: {id1} == {id2}. flock 직렬화 실패 가능성."
    )


# ─────────────────────────────────────────────────────────────────────────────
# Test 6: cleanup 스크립트 dry-run
# ─────────────────────────────────────────────────────────────────────────────

def test_cleanup_dry_run_lists_without_deleting(tmp_path, monkeypatch):
    """cleanup_stale_task_counter.py --dry-run 실행.

    - 가짜 worktree 구조(.worktrees/fake1, fake2) 생성
    - WORKSPACE를 임시 경로로 패치 후 main() 직접 호출
    - dry-run: 파일 경로 출력, 파일 삭제 안 함
    """
    import importlib
    import importlib.util
    import io
    from contextlib import redirect_stdout

    # 가짜 워크트리 구조 생성
    wt1 = tmp_path / ".worktrees" / "fake1" / "memory"
    wt2 = tmp_path / ".worktrees" / "fake2" / "memory"
    wt1.mkdir(parents=True)
    wt2.mkdir(parents=True)

    counter1 = wt1 / ".task-counter"
    counter2 = wt2 / ".task-counter"
    counter1.write_text("1234", encoding="utf-8")
    counter2.write_text("5678", encoding="utf-8")

    # cleanup_stale_task_counter.py 경로
    script_path = _WORKTREE_ROOT / "scripts" / "cleanup_stale_task_counter.py"
    assert script_path.exists(), f"cleanup 스크립트 없음: {script_path}"

    # 모듈 동적 로드 (캐시 오염 방지 위해 고유 이름)
    spec = importlib.util.spec_from_file_location("_cleanup_stale_tc", str(script_path))
    mod = importlib.util.module_from_spec(spec)  # type: ignore[arg-type]
    spec.loader.exec_module(mod)  # type: ignore[union-attr]

    # WORKSPACE 패치
    monkeypatch.setattr(mod, "WORKSPACE", tmp_path)

    # sys.argv 패치 (--dry-run 전달)
    monkeypatch.setattr(sys, "argv", ["cleanup_stale_task_counter.py", "--dry-run"])

    # stdout 캡처하여 main() 실행
    buf = io.StringIO()
    with redirect_stdout(buf):
        ret = mod.main()
    output = buf.getvalue()

    # 반환 코드 0
    assert ret == 0, f"main() 반환 코드: {ret}"

    # 두 파일 경로가 출력에 포함되어야 함
    assert "fake1" in output, f"dry-run 출력에 fake1 없음:\n{output}"
    assert "fake2" in output, f"dry-run 출력에 fake2 없음:\n{output}"
    assert "--dry-run" in output or "dry-run" in output.lower(), (
        f"dry-run 메시지 없음:\n{output}"
    )

    # 파일이 삭제되지 않았는지 확인
    assert counter1.exists(), f"dry-run임에도 {counter1} 삭제됨"
    assert counter2.exists(), f"dry-run임에도 {counter2} 삭제됨"
    assert counter1.read_text().strip() == "1234"
    assert counter2.read_text().strip() == "5678"
