#!/usr/bin/env python3
"""utils/patch_parser.py 테스트 스위트 (TDD)"""

import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).parent.parent.parent))

from utils.patch_parser import apply_patch, generate_patch, parse_patch, verify_patch


# ---------------------------------------------------------------------------
# 샘플 패치 픽스처
# ---------------------------------------------------------------------------

SIMPLE_PATCH = """\
--- a/hello.py
+++ b/hello.py
@@ -1,3 +1,3 @@
 line1
-line2
+line2_modified
 line3
"""

MULTI_HUNK_PATCH = """\
--- a/multi.py
+++ b/multi.py
@@ -1,4 +1,4 @@
 alpha
-beta
+beta_new
 gamma
 delta
@@ -10,4 +10,4 @@
 echo
-foxtrot
+foxtrot_new
 golf
 hotel
"""

ADDED_LINES_PATCH = """\
--- a/add.py
+++ b/add.py
@@ -1,2 +1,4 @@
 first
 second
+third
+fourth
"""

DELETED_LINES_PATCH = """\
--- a/del.py
+++ b/del.py
@@ -1,4 +1,2 @@
 keep1
-remove1
-remove2
 keep2
"""


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


class TestParsePatch:
    """parse_patch() 단위 테스트"""

    def test_simple_patch_returns_one_file_entry(self):
        """단순 패치 파싱 — 파일 엔트리 1개"""
        result = parse_patch(SIMPLE_PATCH)
        assert len(result) == 1

    def test_simple_patch_file_names(self):
        """old_file / new_file 경로 파싱"""
        result = parse_patch(SIMPLE_PATCH)
        assert result[0]["old_file"] == "a/hello.py"
        assert result[0]["new_file"] == "b/hello.py"

    def test_simple_patch_hunk_count(self):
        """단순 패치 hunk 1개"""
        result = parse_patch(SIMPLE_PATCH)
        assert len(result[0]["hunks"]) == 1

    def test_simple_patch_hunk_header(self):
        """hunk 헤더 파싱 (old_start, old_count, new_start, new_count)"""
        hunk = parse_patch(SIMPLE_PATCH)[0]["hunks"][0]
        assert hunk["old_start"] == 1
        assert hunk["old_count"] == 3
        assert hunk["new_start"] == 1
        assert hunk["new_count"] == 3

    def test_simple_patch_hunk_lines(self):
        """hunk lines 내용 확인"""
        hunk = parse_patch(SIMPLE_PATCH)[0]["hunks"][0]
        assert " line1" in hunk["lines"]
        assert "-line2" in hunk["lines"]
        assert "+line2_modified" in hunk["lines"]

    def test_multi_hunk_patch_count(self):
        """여러 hunk 파싱 — hunk 2개"""
        result = parse_patch(MULTI_HUNK_PATCH)
        assert len(result[0]["hunks"]) == 2

    def test_multi_hunk_second_hunk_header(self):
        """두 번째 hunk 헤더 파싱"""
        hunk = parse_patch(MULTI_HUNK_PATCH)[0]["hunks"][1]
        assert hunk["old_start"] == 10
        assert hunk["new_start"] == 10

    def test_empty_patch_returns_empty_list(self):
        """빈 패치 텍스트 → 빈 리스트"""
        result = parse_patch("")
        assert result == []

    def test_whitespace_only_patch_returns_empty_list(self):
        """공백만 있는 패치 → 빈 리스트"""
        result = parse_patch("   \n\n   ")
        assert result == []

    def test_added_lines_patch_hunk_counts(self):
        """추가 라인 패치 hunk count 확인"""
        hunk = parse_patch(ADDED_LINES_PATCH)[0]["hunks"][0]
        assert hunk["old_count"] == 2
        assert hunk["new_count"] == 4

    def test_deleted_lines_patch_hunk_counts(self):
        """삭제 라인 패치 hunk count 확인"""
        hunk = parse_patch(DELETED_LINES_PATCH)[0]["hunks"][0]
        assert hunk["old_count"] == 4
        assert hunk["new_count"] == 2


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


class TestGeneratePatch:
    """generate_patch() 단위 테스트"""

    def test_generate_produces_unified_diff_header(self):
        """unified diff --- / +++ 헤더 생성"""
        patch = generate_patch("line1\nline2\n", "line1\nline2_new\n")
        assert "---" in patch
        assert "+++" in patch

    def test_generate_with_custom_filenames(self):
        """fromfile / tofile 파라미터 반영"""
        patch = generate_patch("old\n", "new\n", fromfile="src.py", tofile="dst.py")
        assert "src.py" in patch
        assert "dst.py" in patch

    def test_generate_identical_content_empty_patch(self):
        """동일 내용 → 빈 패치"""
        patch = generate_patch("same\n", "same\n")
        assert patch == ""

    def test_generate_patch_parseable(self):
        """생성된 패치가 parse_patch로 파싱 가능"""
        patch = generate_patch("a\nb\nc\n", "a\nb_new\nc\n")
        assert patch != ""
        result = parse_patch(patch)
        assert len(result) >= 1


# ---------------------------------------------------------------------------
# apply_patch 및 verify_patch 테스트
# ---------------------------------------------------------------------------


class TestApplyPatch:
    """apply_patch() / verify_patch() 통합 테스트"""

    def test_apply_simple_patch_success(self, tmp_path):
        """단순 패치 적용 성공"""
        target = tmp_path / "hello.py"
        target.write_text("line1\nline2\nline3\n")
        patch = generate_patch("line1\nline2\nline3\n", "line1\nline2_modified\nline3\n", fromfile="a/hello.py", tofile="b/hello.py")
        result = apply_patch(target, patch)
        assert result is True
        assert target.read_text() == "line1\nline2_modified\nline3\n"

    def test_apply_patch_modifies_file(self, tmp_path):
        """패치 적용 후 파일 내용 변경 확인"""
        target = tmp_path / "file.txt"
        original = "alpha\nbeta\ngamma\n"
        modified = "alpha\nbeta_changed\ngamma\n"
        target.write_text(original)
        patch = generate_patch(original, modified)
        apply_patch(target, patch)
        assert target.read_text() == modified

    def test_apply_patch_context_mismatch_returns_false(self, tmp_path):
        """컨텍스트 불일치 시 False 반환"""
        target = tmp_path / "mismatch.py"
        target.write_text("completely\ndifferent\ncontent\n")
        result = apply_patch(target, SIMPLE_PATCH)
        assert result is False

    def test_apply_patch_file_not_modified_on_failure(self, tmp_path):
        """패치 실패 시 원본 파일 유지"""
        target = tmp_path / "safe.py"
        original = "completely\ndifferent\ncontent\n"
        target.write_text(original)
        apply_patch(target, SIMPLE_PATCH)
        assert target.read_text() == original

    def test_apply_patch_nonexistent_file_returns_false(self, tmp_path):
        """존재하지 않는 파일에 패치 시 False 반환"""
        result = apply_patch(tmp_path / "ghost.py", SIMPLE_PATCH)
        assert result is False

    def test_apply_patch_accepts_string_path(self, tmp_path):
        """문자열 경로로 apply_patch 호출 가능"""
        target = tmp_path / "str_path.txt"
        original = "line1\nline2\nline3\n"
        target.write_text(original)
        patch = generate_patch(original, "line1\nline2_mod\nline3\n")
        result = apply_patch(str(target), patch)
        assert result is True

    def test_roundtrip_generate_then_apply(self, tmp_path):
        """generate_patch → apply_patch 라운드트립"""
        original = "foo\nbar\nbaz\n"
        modified = "foo\nbar_updated\nbaz\nqux\n"
        target = tmp_path / "roundtrip.txt"
        target.write_text(original)
        patch = generate_patch(original, modified)
        result = apply_patch(target, patch)
        assert result is True
        assert target.read_text() == modified

    def test_verify_patch_returns_true_for_valid(self, tmp_path):
        """verify_patch — 적용 가능한 패치에 True"""
        target = tmp_path / "verify.txt"
        original = "line1\nline2\nline3\n"
        target.write_text(original)
        patch = generate_patch(original, "line1\nline2_mod\nline3\n")
        assert verify_patch(target, patch) is True

    def test_verify_patch_returns_false_for_mismatch(self, tmp_path):
        """verify_patch — 컨텍스트 불일치에 False"""
        target = tmp_path / "no_match.py"
        target.write_text("completely\ndifferent\ncontent\n")
        assert verify_patch(target, SIMPLE_PATCH) is False

    def test_verify_patch_does_not_modify_file(self, tmp_path):
        """verify_patch 호출 후 파일 내용 변경 없음"""
        target = tmp_path / "unchanged.txt"
        original = "line1\nline2\nline3\n"
        target.write_text(original)
        patch = generate_patch(original, "line1\nline2_mod\nline3\n")
        verify_patch(target, patch)
        assert target.read_text() == original

    def test_apply_empty_patch_returns_true(self, tmp_path):
        """빈 패치(변경 없음) 적용 시 True 반환"""
        target = tmp_path / "no_change.txt"
        target.write_text("content\n")
        result = apply_patch(target, "")
        assert result is True

    def test_apply_added_lines_patch(self, tmp_path):
        """라인 추가 패치 적용"""
        target = tmp_path / "add.txt"
        original = "first\nsecond\n"
        modified = "first\nsecond\nthird\nfourth\n"
        target.write_text(original)
        patch = generate_patch(original, modified)
        result = apply_patch(target, patch)
        assert result is True
        assert target.read_text() == modified

    def test_apply_deleted_lines_patch(self, tmp_path):
        """라인 삭제 패치 적용"""
        target = tmp_path / "del.txt"
        original = "keep1\nremove1\nremove2\nkeep2\n"
        modified = "keep1\nkeep2\n"
        target.write_text(original)
        patch = generate_patch(original, modified)
        result = apply_patch(target, patch)
        assert result is True
        assert target.read_text() == modified

    def test_verify_patch_nonexistent_file_returns_false(self, tmp_path):
        """존재하지 않는 파일 verify_patch → False"""
        assert verify_patch(tmp_path / "missing.txt", SIMPLE_PATCH) is False

    def test_apply_multiline_unicode_content(self, tmp_path):
        """유니코드 내용 파일에 패치 적용"""
        target = tmp_path / "unicode.txt"
        original = "안녕\n세계\n끝\n"
        modified = "안녕\n세상\n끝\n"
        target.write_text(original, encoding="utf-8")
        patch = generate_patch(original, modified)
        result = apply_patch(target, patch)
        assert result is True
        assert target.read_text(encoding="utf-8") == modified


if __name__ == "__main__":
    pytest.main([__file__, "-v"])
