"""
Tests for debug file management (v1.5.0 Phase 2)

Tests debug file saving, cleanup, and security features.
"""

import pytest
from pathlib import Path
from datetime import datetime, timedelta
import time
from unittest.mock import patch

from starpunk.media import cleanup_old_debug_files, validate_image


class TestDebugFileConfiguration:
    """Test debug file configuration options"""

    def test_debug_save_disabled_by_default(self, app):
        """Test that debug file saving is disabled by default"""
        assert app.config.get('DEBUG_SAVE_FAILED_UPLOADS') is False

    def test_debug_max_age_default(self, app):
        """Test that debug file max age has correct default"""
        assert app.config.get('DEBUG_FILE_MAX_AGE_DAYS') == 7

    def test_debug_max_size_default(self, app):
        """Test that debug file max size has correct default"""
        assert app.config.get('DEBUG_FILE_MAX_SIZE_MB') == 100

    def test_debug_config_override(self, app):
        """Test that debug config can be overridden"""
        # Override config
        app.config['DEBUG_SAVE_FAILED_UPLOADS'] = True
        app.config['DEBUG_FILE_MAX_AGE_DAYS'] = 3
        app.config['DEBUG_FILE_MAX_SIZE_MB'] = 50

        assert app.config.get('DEBUG_SAVE_FAILED_UPLOADS') is True
        assert app.config.get('DEBUG_FILE_MAX_AGE_DAYS') == 3
        assert app.config.get('DEBUG_FILE_MAX_SIZE_MB') == 50


class TestDebugFileSaving:
    """Test debug file saving behavior"""

    def test_no_debug_files_when_disabled(self, app):
        """Test that debug files are not saved when feature is disabled"""
        with app.app_context():
            # Ensure disabled
            app.config['DEBUG_SAVE_FAILED_UPLOADS'] = False

            # Try to validate invalid file (should fail and NOT save debug file)
            with pytest.raises(ValueError):
                validate_image(b'not-an-image', 'test.jpg')

            # Check that no debug files were created
            debug_dir = Path(app.config['DATA_PATH']) / 'debug'
            if debug_dir.exists():
                debug_files = list(debug_dir.glob('failed_*'))
                assert len(debug_files) == 0

    def test_debug_files_saved_when_enabled(self, app):
        """Test that debug files are saved when feature is enabled"""
        with app.app_context():
            # Enable debug file saving
            app.config['DEBUG_SAVE_FAILED_UPLOADS'] = True

            # Ensure debug directory exists
            debug_dir = Path(app.config['DATA_PATH']) / 'debug'
            debug_dir.mkdir(parents=True, exist_ok=True)

            # Try to validate invalid file (should fail and save debug file)
            with pytest.raises(ValueError):
                validate_image(b'not-an-image', 'test.jpg')

            # Check that debug file was created
            debug_files = list(debug_dir.glob('failed_*'))
            assert len(debug_files) == 1

            # Verify file contains the corrupt data
            saved_data = debug_files[0].read_bytes()
            assert saved_data == b'not-an-image'


class TestDebugFilenameSanitization:
    """Test filename sanitization for security"""

    def test_path_traversal_prevention(self, app):
        """Test that path traversal attempts are sanitized"""
        with app.app_context():
            # Enable debug file saving
            app.config['DEBUG_SAVE_FAILED_UPLOADS'] = True

            debug_dir = Path(app.config['DATA_PATH']) / 'debug'
            debug_dir.mkdir(parents=True, exist_ok=True)

            # Try to validate with malicious filename
            with pytest.raises(ValueError):
                validate_image(b'not-an-image', '../../../etc/passwd')

            # Check that debug file was created with sanitized name
            debug_files = list(debug_dir.glob('failed_*'))
            assert len(debug_files) == 1

            # Verify filename is sanitized (no path traversal)
            filename = debug_files[0].name
            # Dots are allowed (they're valid), but slashes should be removed
            assert '/' not in filename
            # The filename should contain sanitized version (dots and alphanumerics kept)
            assert 'etcpasswd' in filename  # Slashes removed
            # File should be in debug_dir (not escaped via path traversal)
            assert debug_files[0].parent == debug_dir

    def test_special_characters_removed(self, app):
        """Test that special characters are removed from filenames"""
        with app.app_context():
            # Enable debug file saving
            app.config['DEBUG_SAVE_FAILED_UPLOADS'] = True

            debug_dir = Path(app.config['DATA_PATH']) / 'debug'
            debug_dir.mkdir(parents=True, exist_ok=True)

            # Try to validate with special characters in filename
            with pytest.raises(ValueError):
                validate_image(b'not-an-image', 'test<>:"|?*.jpg')

            # Check that debug file was created with sanitized name
            debug_files = list(debug_dir.glob('failed_*'))
            assert len(debug_files) == 1

            # Verify special characters removed (only alphanumeric, dot, dash, underscore allowed)
            filename = debug_files[0].name
            # Should contain 'test.jpg' but no special chars
            assert 'test' in filename
            assert '.jpg' in filename
            for char in '<>:"|?*':
                assert char not in filename

    def test_long_filename_truncated(self, app):
        """Test that long filenames are truncated to 50 chars"""
        with app.app_context():
            # Enable debug file saving
            app.config['DEBUG_SAVE_FAILED_UPLOADS'] = True

            debug_dir = Path(app.config['DATA_PATH']) / 'debug'
            debug_dir.mkdir(parents=True, exist_ok=True)

            # Try to validate with very long filename
            long_name = 'a' * 100 + '.jpg'
            with pytest.raises(ValueError):
                validate_image(b'not-an-image', long_name)

            # Check that debug file was created
            debug_files = list(debug_dir.glob('failed_*'))
            assert len(debug_files) == 1

            # Verify filename is truncated (the sanitized part should be <=50 chars)
            filename = debug_files[0].name
            # Format is: failed_YYYYMMDD_HHMMSS_sanitized.ext
            # Extract just the sanitized original filename part (after date and time)
            parts = filename.split('_', 3)  # Split on first three underscores
            if len(parts) >= 4:
                sanitized_part = parts[3]
                # Sanitized part should be <= 50 chars (truncation happens before adding to filename)
                assert len(sanitized_part) <= 50


class TestDebugFileCleanup:
    """Test debug file cleanup functionality"""

    def test_cleanup_old_files(self, app):
        """Test that files older than max age are deleted"""
        debug_dir = Path(app.config['DATA_PATH']) / 'debug'
        debug_dir.mkdir(parents=True, exist_ok=True)

        # Create old file (8 days old)
        old_file = debug_dir / 'failed_20250101_120000_old.jpg'
        old_file.write_bytes(b'old data')
        old_time = datetime.now() - timedelta(days=8)
        old_timestamp = old_time.timestamp()
        old_file.touch()
        # Set modification time to 8 days ago
        import os
        os.utime(old_file, (old_timestamp, old_timestamp))

        # Create recent file (3 days old)
        recent_file = debug_dir / 'failed_20250110_120000_recent.jpg'
        recent_file.write_bytes(b'recent data')
        recent_time = datetime.now() - timedelta(days=3)
        recent_timestamp = recent_time.timestamp()
        recent_file.touch()
        os.utime(recent_file, (recent_timestamp, recent_timestamp))

        # Set max age to 7 days
        app.config['DEBUG_FILE_MAX_AGE_DAYS'] = 7

        # Run cleanup
        cleanup_old_debug_files(app)

        # Old file should be deleted
        assert not old_file.exists()

        # Recent file should still exist
        assert recent_file.exists()

    def test_cleanup_size_limit(self, app):
        """Test that oldest files are deleted when size limit exceeded"""
        debug_dir = Path(app.config['DATA_PATH']) / 'debug'
        debug_dir.mkdir(parents=True, exist_ok=True)

        # Create multiple files with known sizes
        # Each file is 30MB (total 90MB)
        file_size = 30 * 1024 * 1024
        data = b'x' * file_size

        import os
        files = []
        for i in range(3):
            file_path = debug_dir / f'failed_2025010{i}_120000_file{i}.jpg'
            file_path.write_bytes(data)
            # Set different modification times with clear spacing
            # Oldest file: 3 days ago, newest file: today
            mtime = datetime.now() - timedelta(days=3-i)
            timestamp = mtime.timestamp()
            os.utime(file_path, (timestamp, timestamp))
            files.append(file_path)

        # Verify files are ordered correctly by mtime
        mtimes = [f.stat().st_mtime for f in files]
        assert mtimes[0] < mtimes[1] < mtimes[2], "Files not properly time-ordered"

        # Set size limit to 70MB (total is 90MB, so should delete oldest to get under limit)
        app.config['DEBUG_FILE_MAX_SIZE_MB'] = 70
        app.config['DEBUG_FILE_MAX_AGE_DAYS'] = 365  # Don't delete by age

        # Run cleanup
        cleanup_old_debug_files(app)

        # Oldest file should be deleted (90MB - 30MB = 60MB, under 70MB limit)
        assert not files[0].exists(), "Oldest file should be deleted for size limit"

        # Two newest files should remain (total 60MB, under 70MB limit)
        assert files[1].exists(), "Second file should remain"
        assert files[2].exists(), "Newest file should remain"

    def test_cleanup_no_files(self, app):
        """Test that cleanup handles empty directory gracefully"""
        debug_dir = Path(app.config['DATA_PATH']) / 'debug'
        debug_dir.mkdir(parents=True, exist_ok=True)

        # Run cleanup on empty directory (should not raise error)
        cleanup_old_debug_files(app)

    def test_cleanup_nonexistent_directory(self, app):
        """Test that cleanup handles nonexistent directory gracefully"""
        debug_dir = Path(app.config['DATA_PATH']) / 'debug'

        # Ensure directory doesn't exist
        if debug_dir.exists():
            import shutil
            shutil.rmtree(debug_dir)

        # Run cleanup (should not raise error)
        cleanup_old_debug_files(app)

    def test_cleanup_combined_age_and_size(self, app):
        """Test cleanup with both age and size limits"""
        debug_dir = Path(app.config['DATA_PATH']) / 'debug'
        debug_dir.mkdir(parents=True, exist_ok=True)

        # Create old large file (should be deleted by age)
        old_file = debug_dir / 'failed_20250101_120000_old.jpg'
        old_file.write_bytes(b'x' * (40 * 1024 * 1024))  # 40MB
        old_time = datetime.now() - timedelta(days=10)
        import os
        os.utime(old_file, (old_time.timestamp(), old_time.timestamp()))

        # Create recent files (total 70MB)
        recent_files = []
        for i in range(2):
            recent_file = debug_dir / f'failed_2025011{i}_120000_recent{i}.jpg'
            recent_file.write_bytes(b'x' * (35 * 1024 * 1024))  # 35MB each
            # i=0: today (newest), i=1: 1 day ago (older)
            # But we want i=0 to be older so it gets deleted first for size limit
            recent_time = datetime.now() - timedelta(days=1+i)  # 1-2 days ago
            os.utime(recent_file, (recent_time.timestamp(), recent_time.timestamp()))
            recent_files.append(recent_file)
            time.sleep(0.01)

        # Set limits
        app.config['DEBUG_FILE_MAX_AGE_DAYS'] = 7
        app.config['DEBUG_FILE_MAX_SIZE_MB'] = 40  # 70MB total, need to delete 30MB

        # Run cleanup
        cleanup_old_debug_files(app)

        # Old file should be deleted by age
        assert not old_file.exists()

        # Of recent files (70MB total), one should be deleted to meet 40MB size limit
        # Exactly one recent file should remain (35MB, under 40MB limit)
        remaining_files = [f for f in recent_files if f.exists()]
        assert len(remaining_files) == 1, "Exactly one recent file should remain after size cleanup"

        # The remaining file should be 35MB
        assert remaining_files[0].stat().st_size == 35 * 1024 * 1024


class TestDebugFileStartupCleanup:
    """Test that cleanup runs on application startup"""

    def test_cleanup_runs_on_startup(self):
        """Test that cleanup runs during app initialization"""
        # Create app with debug files
        from starpunk import create_app
        import tempfile
        import shutil

        # Create temporary data directory
        tmpdir = tempfile.mkdtemp()

        try:
            # Create debug directory with old file
            debug_dir = Path(tmpdir) / 'debug'
            debug_dir.mkdir(parents=True, exist_ok=True)

            old_file = debug_dir / 'failed_20250101_120000_old.jpg'
            old_file.write_bytes(b'old data')
            old_time = datetime.now() - timedelta(days=10)
            import os
            os.utime(old_file, (old_time.timestamp(), old_time.timestamp()))

            # Create app (should run cleanup)
            app = create_app({
                'DATA_PATH': Path(tmpdir),
                'NOTES_PATH': Path(tmpdir) / 'notes',
                'DATABASE_PATH': Path(tmpdir) / 'test.db',
                'DEBUG_FILE_MAX_AGE_DAYS': 7,
                'TESTING': True,
                'SESSION_SECRET': 'test-secret',
                'ADMIN_ME': 'https://test.example.com'
            })

            # Old file should have been cleaned up
            assert not old_file.exists()

        finally:
            # Cleanup temp directory
            shutil.rmtree(tmpdir)
