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

import sys
import time
from pathlib import Path

import pytest

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

from utils.checkpoint import cleanup_old, list_checkpoints, restore, snapshot


class TestSnapshot:
    """snapshot() 함수 테스트"""

    def test_snapshot_creates_backup_file(self, tmp_path: Path) -> None:
        """snapshot이 백업 파일을 생성한다"""
        source = tmp_path / "source.txt"
        source.write_text("original content")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, label="test", checkpoint_dir=checkpoint_dir)

        assert backup_path.exists()

    def test_snapshot_returns_path(self, tmp_path: Path) -> None:
        """snapshot이 Path 객체를 반환한다"""
        source = tmp_path / "source.txt"
        source.write_text("content")

        checkpoint_dir = tmp_path / "checkpoints"
        result = snapshot(source, checkpoint_dir=checkpoint_dir)

        assert isinstance(result, Path)

    def test_snapshot_preserves_content(self, tmp_path: Path) -> None:
        """백업 파일 내용이 원본과 동일하다"""
        source = tmp_path / "source.txt"
        original = "hello world\nline 2"
        source.write_text(original)

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        assert backup_path.read_text() == original

    def test_snapshot_filename_contains_timestamp(self, tmp_path: Path) -> None:
        """백업 파일명에 타임스탬프 형식이 포함된다 (YYYYMMDD_HHMMSS_ffffff)"""
        source = tmp_path / "myfile.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        # 파일명에서 타임스탬프 패턴 확인
        import re

        assert re.search(r"\d{8}_\d{6}_\d{6}", backup_path.name)

    def test_snapshot_filename_contains_original_name(self, tmp_path: Path) -> None:
        """백업 파일명에 원본 파일명이 포함된다"""
        source = tmp_path / "important.py"
        source.write_text("code")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        assert "important.py" in backup_path.name

    def test_snapshot_with_label(self, tmp_path: Path) -> None:
        """label이 파일명에 포함된다"""
        source = tmp_path / "data.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, label="before_refactor", checkpoint_dir=checkpoint_dir)

        assert "before_refactor" in backup_path.name

    def test_snapshot_without_label(self, tmp_path: Path) -> None:
        """label 없이도 스냅샷 생성 가능"""
        source = tmp_path / "file.txt"
        source.write_text("content")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        assert backup_path.exists()

    def test_snapshot_nonexistent_file_raises(self, tmp_path: Path) -> None:
        """존재하지 않는 파일 스냅샷 시 FileNotFoundError"""
        source = tmp_path / "nonexistent.txt"
        checkpoint_dir = tmp_path / "checkpoints"

        with pytest.raises(FileNotFoundError):
            snapshot(source, checkpoint_dir=checkpoint_dir)

    def test_snapshot_creates_checkpoint_dir(self, tmp_path: Path) -> None:
        """체크포인트 디렉토리가 없으면 자동 생성"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "deep" / "nested" / "checkpoints"
        assert not checkpoint_dir.exists()

        snapshot(source, checkpoint_dir=checkpoint_dir)

        assert checkpoint_dir.exists()

    def test_snapshot_multiple_creates_multiple_files(self, tmp_path: Path) -> None:
        """같은 파일을 여러 번 스냅샷 → 여러 백업 파일 생성"""
        source = tmp_path / "file.txt"
        source.write_text("v1")

        checkpoint_dir = tmp_path / "checkpoints"
        path1 = snapshot(source, label="v1", checkpoint_dir=checkpoint_dir)

        time.sleep(0.01)  # 타임스탬프 차이 보장
        source.write_text("v2")
        path2 = snapshot(source, label="v2", checkpoint_dir=checkpoint_dir)

        assert path1 != path2
        assert path1.exists()
        assert path2.exists()


class TestRestore:
    """restore() 함수 테스트"""

    def test_restore_recovers_content(self, tmp_path: Path) -> None:
        """restore가 파일 내용을 복원한다"""
        source = tmp_path / "target.txt"
        source.write_text("original")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        # 파일 수정
        source.write_text("modified")
        assert source.read_text() == "modified"

        # 복원
        restore(backup_path, source)
        assert source.read_text() == "original"

    def test_restore_to_new_path(self, tmp_path: Path) -> None:
        """다른 경로로 복원 가능"""
        source = tmp_path / "source.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        target = tmp_path / "restored.txt"
        restore(backup_path, target)

        assert target.read_text() == "data"

    def test_restore_nonexistent_checkpoint_raises(self, tmp_path: Path) -> None:
        """존재하지 않는 체크포인트 복원 시 FileNotFoundError"""
        fake_checkpoint = tmp_path / "nonexistent_checkpoint.txt"
        target = tmp_path / "target.txt"

        with pytest.raises(FileNotFoundError):
            restore(fake_checkpoint, target)

    def test_restore_accepts_string_paths(self, tmp_path: Path) -> None:
        """str 타입 경로도 허용"""
        source = tmp_path / "file.txt"
        source.write_text("content")

        checkpoint_dir = tmp_path / "checkpoints"
        backup_path = snapshot(source, checkpoint_dir=checkpoint_dir)

        target = tmp_path / "restored.txt"
        restore(str(backup_path), str(target))

        assert target.read_text() == "content"


class TestListCheckpoints:
    """list_checkpoints() 함수 테스트"""

    def test_list_returns_list_of_dicts(self, tmp_path: Path) -> None:
        """list_checkpoints가 dict 리스트를 반환한다"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(source, checkpoint_dir=checkpoint_dir)

        result = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert isinstance(result, list)
        assert len(result) > 0
        assert isinstance(result[0], dict)

    def test_list_dict_has_required_keys(self, tmp_path: Path) -> None:
        """반환 dict에 필수 키가 있다: path, label, timestamp, original_path"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(source, label="mylabel", checkpoint_dir=checkpoint_dir)

        result = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert len(result) == 1
        entry = result[0]

        assert "path" in entry
        assert "label" in entry
        assert "timestamp" in entry
        assert "original_path" in entry

    def test_list_sorted_newest_first(self, tmp_path: Path) -> None:
        """최신 체크포인트가 먼저 온다"""
        source = tmp_path / "file.txt"
        source.write_text("v1")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(source, label="first", checkpoint_dir=checkpoint_dir)

        time.sleep(0.02)
        source.write_text("v2")
        snapshot(source, label="second", checkpoint_dir=checkpoint_dir)

        result = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert len(result) == 2
        # 최신이 먼저
        assert result[0]["label"] == "second"
        assert result[1]["label"] == "first"

    def test_list_label_in_result(self, tmp_path: Path) -> None:
        """label이 결과에 포함된다"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(source, label="my_label", checkpoint_dir=checkpoint_dir)

        result = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert result[0]["label"] == "my_label"

    def test_list_empty_for_no_checkpoints(self, tmp_path: Path) -> None:
        """체크포인트가 없으면 빈 리스트 반환"""
        source = tmp_path / "file.txt"
        checkpoint_dir = tmp_path / "checkpoints"

        result = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert result == []

    def test_list_only_shows_matching_file(self, tmp_path: Path) -> None:
        """다른 파일의 체크포인트는 포함되지 않는다"""
        file_a = tmp_path / "a.txt"
        file_b = tmp_path / "b.txt"
        file_a.write_text("a content")
        file_b.write_text("b content")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(file_a, label="snap_a", checkpoint_dir=checkpoint_dir)
        snapshot(file_b, label="snap_b", checkpoint_dir=checkpoint_dir)

        result_a = list_checkpoints(file_a, checkpoint_dir=checkpoint_dir)
        assert len(result_a) == 1
        assert result_a[0]["label"] == "snap_a"


class TestCleanupOld:
    """cleanup_old() 함수 테스트"""

    def test_cleanup_removes_old_checkpoints(self, tmp_path: Path) -> None:
        """keep 개수 초과 체크포인트를 삭제한다"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"

        for i in range(5):
            time.sleep(0.01)
            snapshot(source, label=f"snap_{i}", checkpoint_dir=checkpoint_dir)

        deleted = cleanup_old(source, keep=3, checkpoint_dir=checkpoint_dir)

        assert deleted == 2
        remaining = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert len(remaining) == 3

    def test_cleanup_keeps_newest(self, tmp_path: Path) -> None:
        """정리 후 최신 체크포인트가 남는다"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"

        for i in range(4):
            time.sleep(0.01)
            snapshot(source, label=f"snap_{i}", checkpoint_dir=checkpoint_dir)

        cleanup_old(source, keep=2, checkpoint_dir=checkpoint_dir)

        remaining = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert len(remaining) == 2
        assert remaining[0]["label"] == "snap_3"
        assert remaining[1]["label"] == "snap_2"

    def test_cleanup_returns_zero_when_nothing_to_delete(self, tmp_path: Path) -> None:
        """삭제할 것이 없으면 0 반환"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"
        snapshot(source, checkpoint_dir=checkpoint_dir)

        deleted = cleanup_old(source, keep=10, checkpoint_dir=checkpoint_dir)
        assert deleted == 0

    def test_cleanup_returns_deleted_count(self, tmp_path: Path) -> None:
        """삭제된 파일 수를 반환한다"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"

        for i in range(6):
            time.sleep(0.01)
            snapshot(source, label=f"s{i}", checkpoint_dir=checkpoint_dir)

        deleted = cleanup_old(source, keep=2, checkpoint_dir=checkpoint_dir)
        assert deleted == 4

    def test_cleanup_default_keep_is_10(self, tmp_path: Path) -> None:
        """기본 keep=10"""
        source = tmp_path / "file.txt"
        source.write_text("data")

        checkpoint_dir = tmp_path / "checkpoints"

        for i in range(12):
            time.sleep(0.01)
            snapshot(source, label=f"s{i}", checkpoint_dir=checkpoint_dir)

        deleted = cleanup_old(source, checkpoint_dir=checkpoint_dir)
        assert deleted == 2
        remaining = list_checkpoints(source, checkpoint_dir=checkpoint_dir)
        assert len(remaining) == 10
