"""
tdd_check.py - TDD 순서 검증 verifier

audit-trail.jsonl에서 파일 수정 순서를 분석하여 테스트 파일이
구현 파일보다 먼저 작성되었는지(Test-Driven Development) 확인합니다.

task_id 필드가 audit-trail에 없는 경우 check_files 기반 fallback 검증을 수행합니다.
"""

import json
import os
import sys

DEFAULT_AUDIT_TRAIL_PATH = "/home/jay/workspace/memory/logs/audit-trail.jsonl"

# 테스트 파일 판별에 사용하는 접미사/접두사 패턴
TEST_PREFIXES = ("test_",)
TEST_SUFFIXES = (".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx")
TEST_DIRS = ("/tests/", "/test/")


def _is_test_file(filepath: str) -> bool:
    """
    주어진 파일 경로가 테스트 파일인지 판별합니다.

    판별 기준:
    - 파일명이 'test_'로 시작하는 경우 (Python unittest 관례)
    - 파일명이 '.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx'로 끝나는 경우
    - 경로에 '/tests/' 또는 '/test/' 디렉토리가 포함된 경우

    Args:
        filepath: 검사할 파일 절대경로 또는 상대경로

    Returns:
        테스트 파일이면 True, 아니면 False
    """
    basename = os.path.basename(filepath)

    for prefix in TEST_PREFIXES:
        if basename.startswith(prefix):
            return True

    for suffix in TEST_SUFFIXES:
        if basename.endswith(suffix):
            return True

    # 경로 구분자를 통일하여 디렉토리 포함 여부 확인
    normalized = filepath.replace("\\", "/")
    for test_dir in TEST_DIRS:
        if test_dir in normalized:
            return True

    return False


def _load_audit_entries(trail_path: str) -> tuple[list[dict], list[str]]:
    """
    audit-trail.jsonl 파일을 읽어 엔트리 목록과 파싱 에러 메시지를 반환합니다.

    Args:
        trail_path: audit-trail.jsonl 파일 경로

    Returns:
        (entries, error_messages) 튜플.
        entries: 파싱에 성공한 JSON 객체 목록
        error_messages: 파싱 실패 또는 파일 읽기 오류 메시지 목록
    """
    entries: list[dict] = []
    errors: list[str] = []

    try:
        with open(trail_path, "r", encoding="utf-8") as f:
            for lineno, line in enumerate(f, start=1):
                line = line.strip()
                if not line:
                    continue
                try:
                    entries.append(json.loads(line))
                except json.JSONDecodeError as e:
                    errors.append(f"JSON parse error at line {lineno}: {e}")
    except FileNotFoundError:
        errors.append(f"audit-trail not found: {trail_path}")
    except OSError as e:
        errors.append(f"Failed to read audit-trail: {type(e).__name__}: {e}")

    return entries, errors


def _verify_by_audit_trail(
    task_id: str,
    entries: list[dict],
) -> dict | None:
    """
    audit-trail 엔트리에서 task_id로 필터링하여 TDD 순서를 검증합니다.

    task_id 필드가 존재하는 엔트리가 없으면 None을 반환하여
    fallback 검증으로 위임합니다.

    Args:
        task_id: 검사 대상 task ID
        entries: audit-trail에서 로드한 전체 엔트리 목록 (시간순 정렬 가정)

    Returns:
        검증 결과 dict, 또는 task_id 매칭 엔트리가 없으면 None
    """
    task_entries = [e for e in entries if e.get("task_id") == task_id]

    if not task_entries:
        return None

    # 수정 도구(Edit/Write/NotebookEdit)로 실제 파일을 변경한 엔트리만 추출
    write_tools = {"Edit", "Write", "NotebookEdit"}
    changed: list[dict] = [e for e in task_entries if e.get("tool") in write_tools and e.get("file")]

    if not changed:
        return {
            "status": "SKIP",
            "details": [
                f"task_id='{task_id}' 엔트리 {len(task_entries)}개 발견, "
                "그러나 파일 변경(Edit/Write/NotebookEdit) 없음",
            ],
        }

    # 파일별 최초 수정 타임스탬프 수집
    first_ts: dict[str, str] = {}
    for entry in changed:
        filepath = os.path.normpath(entry["file"])
        ts = entry.get("ts", "")
        if filepath not in first_ts:
            first_ts[filepath] = ts

    test_files = {fp: ts for fp, ts in first_ts.items() if _is_test_file(fp)}
    impl_files = {fp: ts for fp, ts in first_ts.items() if not _is_test_file(fp)}

    details: list[str] = [
        f"audit-trail 기반 검증 (task_id='{task_id}')",
        f"변경 파일 총 {len(first_ts)}개: 테스트 {len(test_files)}개, 구현 {len(impl_files)}개",
    ]

    if not impl_files:
        # 구현 파일 변경 없음 — 테스트만 있거나 아무것도 없음
        if not test_files:
            return {"status": "SKIP", "details": details + ["변경된 파일 없음"]}
        details.append("구현 파일 변경 없음, 테스트 파일만 존재 → PASS")
        return {"status": "PASS", "details": details}

    if not test_files:
        details.append("테스트 파일 없이 구현 파일만 변경됨 → FAIL")
        for fp, ts in sorted(impl_files.items()):
            details.append(f"  IMPL  [{ts}] {fp}")
        return {"status": "FAIL", "details": details}

    # 테스트 파일과 구현 파일 모두 존재 — 순서 비교
    # 테스트 파일의 최초 수정 시각 vs 구현 파일의 최초 수정 시각
    earliest_test_ts = min(test_files.values())
    earliest_impl_ts = min(impl_files.values())

    # 테스트/구현 파일 수정 내역 상세 출력
    for fp, ts in sorted(test_files.items(), key=lambda x: x[1]):
        details.append(f"  TEST  [{ts}] {fp}")
    for fp, ts in sorted(impl_files.items(), key=lambda x: x[1]):
        details.append(f"  IMPL  [{ts}] {fp}")

    if earliest_test_ts <= earliest_impl_ts:
        details.append(f"테스트 먼저 수정 ({earliest_test_ts}) → 구현 ({earliest_impl_ts}) → PASS")
        return {"status": "PASS", "details": details}
    else:
        details.append(f"구현 먼저 수정 ({earliest_impl_ts}) → 테스트 ({earliest_test_ts}) → WARN (TDD 순서 위반)")
        return {"status": "WARN", "details": details}


def _verify_by_check_files(check_files: list[str]) -> dict:
    """
    check_files 목록에서 테스트 파일과 구현 파일을 분리하여 TDD 준수 여부를 검증합니다.

    audit-trail에 task_id가 없는 경우의 fallback 검증입니다.
    파일 수정 순서는 알 수 없으므로 테스트 파일 존재 여부만 확인합니다.

    Args:
        check_files: 검사 대상 파일 경로 목록

    Returns:
        {"status": "PASS"|"FAIL"|"SKIP", "details": [...]}
    """
    if not check_files:
        return {
            "status": "SKIP",
            "details": ["check_files 미제공 — 검증 불가"],
        }

    test_files = [f for f in check_files if _is_test_file(f)]
    impl_files = [f for f in check_files if not _is_test_file(f)]

    details: list[str] = [
        "check_files 기반 검증 (fallback)",
        f"파일 총 {len(check_files)}개: 테스트 {len(test_files)}개, 구현 {len(impl_files)}개",
    ]

    if not impl_files and not test_files:
        return {"status": "SKIP", "details": details + ["검사할 파일 없음"]}

    if not impl_files:
        # 테스트 파일만 있는 경우
        for f in sorted(test_files):
            details.append(f"  TEST  {f}")
        details.append("구현 파일 없음, 테스트 파일만 존재 → PASS")
        return {"status": "PASS", "details": details}

    if not test_files:
        # 구현 파일만 있는 경우
        for f in sorted(impl_files):
            details.append(f"  IMPL  {f}")
        details.append("구현 파일 존재, 대응하는 테스트 파일 없음 → FAIL")
        return {"status": "FAIL", "details": details}

    # 테스트와 구현 파일 모두 존재 (순서 정보 없음 → PASS로 처리)
    for f in sorted(test_files):
        details.append(f"  TEST  {f}")
    for f in sorted(impl_files):
        details.append(f"  IMPL  {f}")
    details.append("테스트 파일과 구현 파일 모두 존재 (수정 순서 미확인) → PASS")
    return {"status": "PASS", "details": details}


def verify(
    task_id: str,
    check_files: list[str] | None = None,
    audit_trail_path: str = "",
) -> dict:
    """
    TDD(Test-Driven Development) 준수 여부를 검증합니다.

    검증 우선순위:
    1. audit-trail.jsonl에 task_id 필드가 있는 엔트리가 존재하면
       파일 수정 타임스탬프 순서를 기반으로 검증합니다.
    2. task_id 매칭 엔트리가 없으면 check_files 목록을 기반으로
       테스트 파일 존재 여부를 확인합니다.

    판정 기준:
    - PASS : 테스트 파일이 구현 파일보다 먼저 수정됨,
             또는 테스트 파일만 존재하거나 check_files에 테스트+구현 모두 존재
    - FAIL : 테스트 파일 없이 구현 파일만 존재
    - WARN : 구현 파일이 테스트 파일보다 먼저 수정됨 (TDD 순서 위반이지만 테스트 존재)
    - SKIP : 검사 대상 파일이 없거나 audit-trail을 읽을 수 없고 check_files도 없는 경우

    Args:
        task_id: 검사 대상 task ID
        check_files: fallback 검증에 사용할 파일 경로 목록 (선택)
        audit_trail_path: audit-trail.jsonl 경로
                          (기본값: DEFAULT_AUDIT_TRAIL_PATH)

    Returns:
        {"status": "PASS"|"FAIL"|"WARN"|"SKIP", "details": [...]}
    """
    trail_path = audit_trail_path if audit_trail_path else DEFAULT_AUDIT_TRAIL_PATH
    details_prefix: list[str] = []

    # audit-trail 로드 시도
    entries, load_errors = _load_audit_entries(trail_path)

    if load_errors:
        # 파일 자체를 읽지 못한 경우 (FileNotFoundError 등)
        if not entries:
            details_prefix.extend(load_errors)
            # audit-trail 없음 → check_files fallback
            if check_files:
                result = _verify_by_check_files(check_files)
                result["details"] = details_prefix + result["details"]
                return result
            return {
                "status": "SKIP",
                "details": details_prefix + ["check_files도 미제공 — 검증 불가"],
            }
        # 일부 라인 파싱 실패 (경고만 추가하고 계속)
        details_prefix.extend(load_errors)

    # audit-trail 기반 검증 시도
    audit_result = _verify_by_audit_trail(task_id, entries)

    if audit_result is not None:
        # task_id 매칭 엔트리 존재 → audit-trail 결과 사용
        if details_prefix:
            audit_result["details"] = details_prefix + audit_result["details"]
        return audit_result

    # task_id 매칭 없음 → fallback
    details_prefix.append(f"audit-trail에 task_id='{task_id}' 매칭 엔트리 없음 — check_files fallback")

    if check_files:
        result = _verify_by_check_files(check_files)
        result["details"] = details_prefix + result["details"]
        return result

    return {
        "status": "SKIP",
        "details": details_prefix + ["check_files도 미제공 — 검증 불가"],
    }


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(
        description="TDD 순서 검증 verifier",
        epilog="예시: tdd_check.py task-42 --check-files src/foo.py,tests/test_foo.py",
    )
    parser.add_argument("task_id", help="검사 대상 task ID")
    parser.add_argument(
        "--check-files",
        metavar="FILE1,FILE2,...",
        default="",
        help="쉼표로 구분된 파일 경로 목록 (fallback 검증용)",
    )
    parser.add_argument(
        "--audit-trail",
        metavar="PATH",
        default="",
        help=f"audit-trail.jsonl 경로 (기본값: {DEFAULT_AUDIT_TRAIL_PATH})",
    )

    args = parser.parse_args()

    cf: list[str] | None = None
    if args.check_files:
        cf = [f.strip() for f in args.check_files.split(",") if f.strip()]

    result = verify(
        task_id=args.task_id,
        check_files=cf,
        audit_trail_path=args.audit_trail,
    )
    print(json.dumps(result, ensure_ascii=False, indent=2))
    sys.exit(0 if result["status"] in ("PASS", "SKIP") else 1)
