"""ast_dependency_map.py 단위 테스트 (task-2337 Phase 1.4)."""
import sys
from pathlib import Path

import pytest  # noqa: F401

# 워크스페이스 scripts/ 를 sys.path에 추가
WORKSPACE = Path("/home/jay/workspace")
sys.path.insert(0, str(WORKSPACE / "scripts"))

import ast_dependency_map as adm  # noqa: E402


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
def isolated_cache(monkeypatch, tmp_path):
    cache = tmp_path / "cache"
    cache.mkdir()
    monkeypatch.setattr(adm, "CACHE_DIR", cache)
    return cache


@pytest.fixture
def tiny_project(tmp_path):
    root = tmp_path / "proj"
    root.mkdir()
    return root


def _write(p: Path, content: str = ""):
    p.parent.mkdir(parents=True, exist_ok=True)
    p.write_text(content)


# ---------------------------------------------------------------------------
# 테스트 케이스 1: EXCLUDE_DIRS 검증
# ---------------------------------------------------------------------------


def test_iter_py_files_excludes_dirs(tmp_path):
    """EXCLUDE_DIRS 목록의 디렉토리는 _iter_py_files() 결과에서 제외되어야 한다."""
    root = tmp_path / "root"

    # 포함되어야 할 파일
    _write(root / "main.py", "# main")
    _write(root / "foo.py", "# foo")

    # 제외되어야 할 파일들
    _write(root / ".worktrees" / "sub" / "excluded.py", "# excluded")
    _write(root / "__pycache__" / "cached.py", "# cached")
    _write(root / ".venv" / "lib" / "site.py", "# site")
    _write(root / ".git" / "hooks" / "pre-commit.py", "# pre-commit")
    _write(root / "node_modules" / "pkg" / "index.py", "# index")
    _write(root / ".codegraph-venv" / "lib" / "x.py", "# x")

    result = adm._iter_py_files(root)
    result_names = {p.name for p in result}

    # 포함 확인
    assert "main.py" in result_names, "main.py 가 결과에 없음"
    assert "foo.py" in result_names, "foo.py 가 결과에 없음"

    # 제외 확인
    assert "excluded.py" not in result_names, ".worktrees 하위 파일이 결과에 포함됨"
    assert "cached.py" not in result_names, "__pycache__ 하위 파일이 결과에 포함됨"
    assert "site.py" not in result_names, ".venv 하위 파일이 결과에 포함됨"
    assert "pre-commit.py" not in result_names, ".git 하위 파일이 결과에 포함됨"
    assert "index.py" not in result_names, "node_modules 하위 파일이 결과에 포함됨"
    assert "x.py" not in result_names, ".codegraph-venv 하위 파일이 결과에 포함됨"

    # 정확히 2개만 반환되어야 함
    assert len(result) == 2, f"예상 2개, 실제 {len(result)}개: {result}"


# ---------------------------------------------------------------------------
# 테스트 케이스 2: 캐시 hit/miss 동작
# ---------------------------------------------------------------------------


def test_cache_roundtrip(isolated_cache, tiny_project):
    """캐시 miss → 저장 → hit 후 동일 그래프 반환을 검증한다."""
    root = tiny_project

    # 임시 프로젝트 구성
    _write(root / "alpha.py", "import beta\n")
    _write(root / "beta.py", "# beta module\n")

    # --- 첫 번째 호출: cache miss → 빌드 → 저장 ---
    g1 = adm.DependencyGraph(root)

    # 캐시 파일이 생성되어야 함
    cache_files = list(isolated_cache.glob("*.pkl"))
    assert len(cache_files) == 1, f"캐시 파일이 1개여야 하는데 {len(cache_files)}개"

    # --- 두 번째 호출: cache hit ---
    g2 = adm.DependencyGraph(root)

    # module_to_file 동일
    assert set(g1.module_to_file.keys()) == set(g2.module_to_file.keys()), \
        "module_to_file 키 불일치"
    for k in g1.module_to_file:
        assert g1.module_to_file[k] == g2.module_to_file[k], \
            f"module_to_file[{k}] 값 불일치"

    # file_imports 동일
    assert set(str(k) for k in g1.file_imports.keys()) == \
           set(str(k) for k in g2.file_imports.keys()), "file_imports 키 불일치"
    for k in g1.file_imports:
        assert g1.file_imports[k] == g2.file_imports[k], \
            f"file_imports[{k}] 값 불일치"

    # module_importers 동일
    assert set(g1.module_importers.keys()) == set(g2.module_importers.keys()), \
        "module_importers 키 불일치"
    for k in g1.module_importers:
        assert g1.module_importers[k] == g2.module_importers[k], \
            f"module_importers[{k}] 값 불일치"


# ---------------------------------------------------------------------------
# 테스트 케이스 3: mtime 변경 시 캐시 무효화
# ---------------------------------------------------------------------------


def test_cache_invalidates_on_mtime_change(isolated_cache, tiny_project):
    """새 파일 추가 후 mtime 변경 시 캐시가 무효화되고 새 그래프에 파일이 포함된다."""
    root = tiny_project

    # 초기 프로젝트: a.py 만 존재
    _write(root / "a.py", "# module a\n")

    # 첫 번째 빌드 → 캐시 저장
    g1 = adm.DependencyGraph(root)
    assert "a" in g1.module_to_file, "a 모듈이 g1에 없음"
    assert "b" not in g1.module_to_file, "b 모듈이 이미 g1에 있으면 안 됨"

    cache_files_before = list(isolated_cache.glob("*.pkl"))
    assert len(cache_files_before) == 1

    # 새 파일 b.py 추가 → mtime 변화
    _write(root / "b.py", "# module b\n")

    # 두 번째 빌드 → cache miss 발생 → 새 그래프
    g2 = adm.DependencyGraph(root)
    assert "a" in g2.module_to_file, "a 모듈이 g2에 없음"
    assert "b" in g2.module_to_file, "b 모듈이 g2에 포함되어야 함 (cache miss 후 재빌드)"

    # 새 캐시 파일이 생성됐는지 확인 (키가 달라졌으므로)
    cache_files_after = list(isolated_cache.glob("*.pkl"))
    # 이전 캐시 키와 달라야 하므로 파일이 2개(이전 + 신규) 또는 1개(덮어쓰기) 존재
    assert len(cache_files_after) >= 1


# ---------------------------------------------------------------------------
# 테스트 케이스 4: 분석 결과에 .worktrees 미포함
# ---------------------------------------------------------------------------


def test_analyze_excludes_worktrees(isolated_cache, tiny_project):
    """.worktrees 하위 파일은 analyze() direct_importers에 포함되지 않아야 한다."""
    root = tiny_project

    # 정상 패키지 파일
    _write(root / "pkg" / "main.py", "import data\n")
    _write(root / "pkg" / "data.py", "# data module\n")

    # .worktrees 하위에 동일 import 구조 (제외 대상)
    _write(root / "pkg" / ".worktrees" / "sub" / "main.py", "import data\n")

    results = adm.analyze(root, ["data.py"])
    assert len(results) == 1

    direct_importers = results[0]["blast_radius"]["direct_importers"]

    # .worktrees 경로가 없어야 함
    for path_str in direct_importers:
        assert ".worktrees" not in path_str, \
            f".worktrees 경로가 direct_importers에 포함됨: {path_str}"


# ---------------------------------------------------------------------------
# 테스트 케이스 5: 함수 콜러 lazy AST
# ---------------------------------------------------------------------------


def test_get_function_callers_lazy_ast(isolated_cache, tiny_project):
    """캐시 hit 후 lazy AST를 사용해 함수 호출자를 올바르게 찾아야 한다."""
    root = tiny_project

    _write(root / "lib.py", "def hello():\n    pass\n")
    _write(root / "caller.py", "from lib import hello\nhello()\n")

    results = adm.analyze(root, ["lib.py"], function_name="hello")
    assert len(results) == 1

    callers = results[0]["blast_radius"]["callers"]
    assert len(callers) > 0, "callers가 비어 있음 — hello() 호출 감지 실패"

    # caller.py:N 형식으로 포함
    caller_files = [c.split(":")[0] for c in callers]
    assert any("caller.py" in f for f in caller_files), \
        f"caller.py 가 callers에 없음: {callers}"

    # 줄 번호 포함 확인
    for c in callers:
        parts = c.rsplit(":", 1)
        assert len(parts) == 2, f"줄번호 없는 caller 항목: {c}"
        assert parts[1].isdigit(), f"줄번호가 숫자가 아님: {c}"


# ---------------------------------------------------------------------------
# 테스트 케이스 6: 빈 affected_files
# ---------------------------------------------------------------------------


def test_analyze_empty_files(isolated_cache, tiny_project):
    """빈 affected_files 리스트로 analyze() 호출 시 빈 리스트 반환, 예외 없음."""
    root = tiny_project
    _write(root / "something.py", "# placeholder\n")

    results = adm.analyze(root, [])
    assert results == [], f"빈 입력 시 빈 리스트 반환 예상, 실제: {results}"
