"""
Tests for safe file operations in sltk/io/safe_write.py.

Tests cover:
- Atomic write operations
- Versioned saving
- Backup creation
- Path generation
- ELAN export integration
- Cleanup utilities
"""

import os
import tempfile
import threading
import time
from pathlib import Path

import pytest

from sltk.io.safe_write import (
    AtomicWriteError,
    BackupError,
    FileExistsError,
    SafeWriteError,
    atomic_write,
    atomic_write_with_callback,
    backup_before_modify,
    backup_file,
    cleanup_old_backups,
    cleanup_old_versions,
    file_lock,
    generate_export_path,
    suggest_safe_path,
    versioned_save,
    versioned_save_with_callback,
)


@pytest.fixture
def temp_dir():
    """Create a temporary directory for tests."""
    with tempfile.TemporaryDirectory() as tmpdir:
        yield Path(tmpdir)


@pytest.fixture
def sample_content():
    """Sample content for writing."""
    return "Test content for safe write operations.\nLine 2.\nLine 3."


@pytest.fixture
def sample_eaf_content():
    """Sample ELAN XML content."""
    return """<?xml version="1.0" encoding="UTF-8"?>
<ANNOTATION_DOCUMENT AUTHOR="Test" FORMAT="3.0">
    <HEADER MEDIA_FILE="" TIME_UNITS="milliseconds"/>
    <TIME_ORDER>
        <TIME_SLOT TIME_SLOT_ID="ts1" TIME_VALUE="0"/>
        <TIME_SLOT TIME_SLOT_ID="ts2" TIME_VALUE="1000"/>
    </TIME_ORDER>
    <TIER LINGUISTIC_TYPE_REF="default-lt" TIER_ID="Gloss">
        <ANNOTATION>
            <ALIGNABLE_ANNOTATION ANNOTATION_ID="a1" TIME_SLOT_REF1="ts1" TIME_SLOT_REF2="ts2">
                <ANNOTATION_VALUE>TEST</ANNOTATION_VALUE>
            </ALIGNABLE_ANNOTATION>
        </ANNOTATION>
    </TIER>
    <LINGUISTIC_TYPE GRAPHIC_REFERENCES="false" LINGUISTIC_TYPE_ID="default-lt" TIME_ALIGNABLE="true"/>
</ANNOTATION_DOCUMENT>"""


class TestAtomicWrite:
    """Tests for atomic_write function."""

    def test_atomic_write_new_file(self, temp_dir, sample_content):
        """Test writing to a new file."""
        path = temp_dir / "new_file.txt"
        result = atomic_write(path, sample_content)

        assert result == path
        assert path.exists()
        assert path.read_text() == sample_content

    def test_atomic_write_creates_parent_dirs(self, temp_dir, sample_content):
        """Test that parent directories are created."""
        path = temp_dir / "nested" / "dirs" / "file.txt"
        result = atomic_write(path, sample_content)

        assert result == path
        assert path.exists()

    def test_atomic_write_refuses_overwrite_by_default(self, temp_dir, sample_content):
        """Test that overwrite is refused by default."""
        path = temp_dir / "existing.txt"
        path.write_text("original content")

        with pytest.raises(FileExistsError):
            atomic_write(path, sample_content, overwrite=False)

        # Original content unchanged
        assert path.read_text() == "original content"

    def test_atomic_write_allows_overwrite_when_specified(self, temp_dir, sample_content):
        """Test that overwrite works when specified."""
        path = temp_dir / "existing.txt"
        path.write_text("original content")

        atomic_write(path, sample_content, overwrite=True)
        assert path.read_text() == sample_content

    def test_atomic_write_bytes(self, temp_dir):
        """Test writing bytes content."""
        path = temp_dir / "binary.bin"
        content = b"\x00\x01\x02\x03\x04"

        atomic_write(path, content)
        assert path.read_bytes() == content

    def test_atomic_write_no_temp_file_on_success(self, temp_dir, sample_content):
        """Test that no temp files remain after successful write."""
        path = temp_dir / "clean.txt"
        atomic_write(path, sample_content)

        # Check no temp files remain
        temp_files = list(temp_dir.glob(".*_tmp_*"))
        assert len(temp_files) == 0


class TestAtomicWriteWithCallback:
    """Tests for atomic_write_with_callback function."""

    def test_callback_write(self, temp_dir):
        """Test writing with a callback function."""
        path = temp_dir / "callback.txt"

        def write_func(p):
            p.write_text("Written via callback")

        result = atomic_write_with_callback(path, write_func)
        assert result == path
        assert path.read_text() == "Written via callback"

    def test_callback_error_cleanup(self, temp_dir):
        """Test that temp files are cleaned up on callback error."""
        path = temp_dir / "error.txt"

        def failing_func(p):
            raise ValueError("Intentional error")

        with pytest.raises(AtomicWriteError):
            atomic_write_with_callback(path, failing_func)

        # No file should be created
        assert not path.exists()
        # No temp files should remain
        temp_files = list(temp_dir.glob(".*_tmp_*"))
        assert len(temp_files) == 0


class TestVersionedSave:
    """Tests for versioned_save function."""

    def test_versioned_save_new_file(self, temp_dir, sample_content):
        """Test saving when base file doesn't exist."""
        path = temp_dir / "output.txt"
        result = versioned_save(path, sample_content)

        # Should use base path directly
        assert result == path
        assert path.exists()

    def test_versioned_save_creates_version(self, temp_dir, sample_content):
        """Test that version is created when base exists."""
        path = temp_dir / "output.txt"
        path.write_text("original")

        result = versioned_save(path, sample_content)

        # Should create versioned file
        assert result == temp_dir / "output_v001.txt"
        assert result.exists()
        assert result.read_text() == sample_content
        # Original unchanged
        assert path.read_text() == "original"

    def test_versioned_save_increments_version(self, temp_dir, sample_content):
        """Test version number increments correctly."""
        path = temp_dir / "output.txt"
        path.write_text("original")
        (temp_dir / "output_v001.txt").write_text("v1")
        (temp_dir / "output_v002.txt").write_text("v2")

        result = versioned_save(path, sample_content)

        assert result == temp_dir / "output_v003.txt"
        assert result.exists()

    def test_versioned_save_max_versions(self, temp_dir, sample_content):
        """Test error when max versions reached."""
        path = temp_dir / "output.txt"
        path.write_text("original")

        # Create max versions (testing with small max)
        for i in range(1, 6):
            (temp_dir / f"output_v{i:03d}.txt").write_text(f"v{i}")

        with pytest.raises(SafeWriteError, match="All version slots taken"):
            versioned_save(path, sample_content, max_versions=5)


class TestBackupFile:
    """Tests for backup_file function."""

    def test_backup_creates_timestamped_copy(self, temp_dir, sample_content):
        """Test that backup creates a timestamped copy."""
        path = temp_dir / "data.txt"
        path.write_text(sample_content)

        backup_path = backup_file(path)

        assert backup_path.exists()
        assert "backup_" in backup_path.name
        assert backup_path.read_text() == sample_content
        # Original unchanged
        assert path.read_text() == sample_content

    def test_backup_to_different_directory(self, temp_dir, sample_content):
        """Test backup to a different directory."""
        path = temp_dir / "data.txt"
        path.write_text(sample_content)
        backup_dir = temp_dir / "backups"

        backup_path = backup_file(path, backup_dir=backup_dir)

        assert backup_path.parent == backup_dir
        assert backup_path.exists()

    def test_backup_nonexistent_file(self, temp_dir):
        """Test backup of non-existent file raises error."""
        path = temp_dir / "nonexistent.txt"

        with pytest.raises(FileNotFoundError):
            backup_file(path)

    def test_backup_before_modify_skip_missing(self, temp_dir):
        """Test backup_before_modify with skip_if_missing."""
        path = temp_dir / "nonexistent.txt"

        result = backup_before_modify(path, skip_if_missing=True)
        assert result is None

    def test_backup_before_modify_existing(self, temp_dir, sample_content):
        """Test backup_before_modify with existing file."""
        path = temp_dir / "data.txt"
        path.write_text(sample_content)

        backup_path = backup_before_modify(path)

        assert backup_path is not None
        assert backup_path.exists()


class TestGenerateExportPath:
    """Tests for generate_export_path function."""

    def test_basic_path_generation(self, temp_dir):
        """Test basic path generation with default suffix."""
        base = temp_dir / "video.eaf"
        result = generate_export_path(base, suffix="_AI")

        assert result.parent == temp_dir
        assert "_AI_" in result.name
        assert result.suffix == ".eaf"

    def test_no_timestamp(self, temp_dir):
        """Test path generation without timestamp."""
        base = temp_dir / "video.eaf"
        result = generate_export_path(base, suffix="_AI", timestamp=False)

        assert result.name == "video_AI.eaf"

    def test_different_output_dir(self, temp_dir):
        """Test path generation with different output directory."""
        base = temp_dir / "video.eaf"
        output_dir = temp_dir / "exports"

        result = generate_export_path(base, output_dir=output_dir)

        assert result.parent == output_dir
        assert output_dir.exists()

    def test_uniqueness_with_existing_file(self, temp_dir):
        """Test that generated path is unique."""
        base = temp_dir / "video.eaf"

        # Generate first path
        path1 = generate_export_path(base, suffix="_AI", timestamp=False)
        path1.write_text("existing")

        # Generate second path - should be different
        path2 = generate_export_path(base, suffix="_AI", timestamp=False)

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


class TestSuggestSafePath:
    """Tests for suggest_safe_path function."""

    def test_suggestions_for_existing_file(self, temp_dir, sample_content):
        """Test suggestions when file exists."""
        path = temp_dir / "annotations.eaf"
        path.write_text(sample_content)

        result = suggest_safe_path(str(path), purpose="export")

        assert result["exists"] is True
        assert "ai_export" in result["suggestions"]
        assert "timestamped" in result["suggestions"]
        assert "backup" in result["suggestions"]
        assert result["recommended"] is not None

    def test_suggestions_for_new_file(self, temp_dir):
        """Test suggestions when file doesn't exist."""
        path = temp_dir / "new.eaf"

        result = suggest_safe_path(str(path), purpose="export")

        assert result["exists"] is False
        assert "ai_export" in result["suggestions"]


class TestFileLock:
    """Tests for file_lock context manager."""

    def test_basic_lock(self, temp_dir):
        """Test basic file locking."""
        path = temp_dir / "lockable.txt"

        with file_lock(path, exclusive=True):
            # Should be able to write while holding lock
            path.write_text("locked write")

        assert path.read_text() == "locked write"

    def test_lock_file_cleanup(self, temp_dir):
        """Test that lock files are cleaned up."""
        path = temp_dir / "lockable.txt"

        with file_lock(path, exclusive=True):
            pass

        # Lock file should be cleaned up
        lock_path = path.with_suffix(path.suffix + ".lock")
        assert not lock_path.exists()

    def test_concurrent_locks_wait(self, temp_dir):
        """Test that concurrent exclusive locks wait."""
        path = temp_dir / "shared.txt"
        results = []

        def writer(delay, value):
            time.sleep(delay)
            with file_lock(path, exclusive=True, timeout=5.0):
                results.append(f"start_{value}")
                time.sleep(0.1)
                results.append(f"end_{value}")

        t1 = threading.Thread(target=writer, args=(0, 1))
        t2 = threading.Thread(target=writer, args=(0.05, 2))

        t1.start()
        t2.start()
        t1.join()
        t2.join()

        # Results should show no interleaving
        # Either start_1, end_1, start_2, end_2 or start_2, end_2, start_1, end_1
        assert results == ["start_1", "end_1", "start_2", "end_2"] or results == [
            "start_2",
            "end_2",
            "start_1",
            "end_1",
        ]


class TestCleanupOldVersions:
    """Tests for cleanup_old_versions function."""

    def test_cleanup_dry_run(self, temp_dir):
        """Test cleanup in dry run mode."""
        base = temp_dir / "output.txt"
        base.write_text("original")

        # Create versions
        for i in range(1, 8):
            (temp_dir / f"output_v{i:03d}.txt").write_text(f"v{i}")

        to_delete = cleanup_old_versions(base, keep_versions=3, dry_run=True)

        # Should report 4 files to delete (keep v005, v006, v007)
        assert len(to_delete) == 4
        # Files should still exist (dry run)
        assert (temp_dir / "output_v001.txt").exists()

    def test_cleanup_actual_delete(self, temp_dir):
        """Test actual cleanup."""
        base = temp_dir / "output.txt"
        base.write_text("original")

        # Create versions
        for i in range(1, 6):
            (temp_dir / f"output_v{i:03d}.txt").write_text(f"v{i}")

        cleanup_old_versions(base, keep_versions=2, dry_run=False)

        # Only latest 2 versions should remain
        assert not (temp_dir / "output_v001.txt").exists()
        assert not (temp_dir / "output_v002.txt").exists()
        assert not (temp_dir / "output_v003.txt").exists()
        assert (temp_dir / "output_v004.txt").exists()
        assert (temp_dir / "output_v005.txt").exists()


class TestCleanupOldBackups:
    """Tests for cleanup_old_backups function."""

    def test_cleanup_old_backups_dry_run(self, temp_dir, sample_content):
        """Test backup cleanup in dry run mode."""
        base = temp_dir / "data.txt"
        base.write_text(sample_content)

        # Create backup files with old timestamps (by modifying mtime)
        old_backup = temp_dir / "data_backup_20200101_120000.txt"
        old_backup.write_text("old backup")
        old_time = time.time() - (30 * 24 * 60 * 60)  # 30 days ago
        os.utime(old_backup, (old_time, old_time))

        # Create recent backup
        recent_backup = temp_dir / "data_backup_20260202_120000.txt"
        recent_backup.write_text("recent backup")

        to_delete = cleanup_old_backups(base, keep_days=7, dry_run=True)

        assert old_backup in to_delete
        assert recent_backup not in to_delete
        # Files still exist (dry run)
        assert old_backup.exists()


class TestSafeElanIntegration:
    """Integration tests for safe ELAN export functions."""

    def test_safe_write_eaf(self, temp_dir):
        """Test safe_write_eaf function."""
        from sltk.data.core import Segment, SegmentList
        from sltk.io.safe_write import safe_write_eaf

        segments = SegmentList(
            [
                Segment(0.0, 1.5, "HELLO", tier="Gloss"),
                Segment(1.5, 3.0, "WORLD", tier="Gloss"),
            ]
        )

        path = temp_dir / "output.eaf"
        result = safe_write_eaf(segments, path)

        assert result == path
        assert path.exists()
        # Verify it's valid XML
        assert path.read_text().startswith("<?xml")

    def test_safe_write_eaf_auto_version(self, temp_dir):
        """Test safe_write_eaf with auto versioning."""
        from sltk.data.core import Segment, SegmentList
        from sltk.io.safe_write import safe_write_eaf

        segments = SegmentList(
            [
                Segment(0.0, 1.5, "TEST", tier="Gloss"),
            ]
        )

        path = temp_dir / "output.eaf"

        # First write
        result1 = safe_write_eaf(segments, path)
        assert result1 == path

        # Second write - should auto-version
        result2 = safe_write_eaf(segments, path, overwrite=False, auto_version=True)
        assert result2 != path
        assert "v001" in result2.name or "v002" in result2.name

    def test_safe_add_tier_to_eaf(self, temp_dir, sample_eaf_content):
        """Test safe_add_tier_to_eaf function."""
        from sltk.data.core import Segment, SegmentList
        from sltk.io.safe_write import safe_add_tier_to_eaf

        # Create source file
        source_path = temp_dir / "original.eaf"
        source_path.write_text(sample_eaf_content)

        segments = SegmentList(
            [
                Segment(0.0, 0.5, "AI_SIGN", tier="AI_Tier"),
            ]
        )

        result = safe_add_tier_to_eaf(
            source_path,
            segments,
            tier_name="AI_Tier",
            backup_original=True,
        )

        assert result["output"].exists()
        assert result["backup"].exists()
        # Output should have AI suffix
        assert "_AI_" in result["output"].name
        # Original unchanged
        assert source_path.read_text() == sample_eaf_content
