"""Unit tests for ConfigService."""

import json
import tempfile
from pathlib import Path

import pytest

from src.server.models.config import (
    AppConfig,
    BackupConfig,
    ConfigUpdate,
    LoggingConfig,
    SchedulerConfig,
)
from src.server.services.config_service import (
    ConfigBackupError,
    ConfigService,
    ConfigServiceError,
    ConfigValidationError,
)


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


@pytest.fixture
def config_service(temp_dir):
    """Create ConfigService instance with temporary paths."""
    config_path = temp_dir / "config.json"
    backup_dir = temp_dir / "backups"
    return ConfigService(
        config_path=config_path, backup_dir=backup_dir, max_backups=3
    )


@pytest.fixture
def sample_config():
    """Create sample configuration."""
    return AppConfig(
        name="TestApp",
        data_dir="test_data",
        scheduler=SchedulerConfig(enabled=True, interval_minutes=30),
        logging=LoggingConfig(level="DEBUG", file="test.log"),
        backup=BackupConfig(enabled=False),
        other={"custom_key": "custom_value"},
    )


class TestConfigServiceInitialization:
    """Test ConfigService initialization and directory creation."""

    def test_initialization_creates_directories(self, temp_dir):
        """Test that initialization creates necessary directories."""
        config_path = temp_dir / "subdir" / "config.json"
        backup_dir = temp_dir / "subdir" / "backups"

        service = ConfigService(config_path=config_path, backup_dir=backup_dir)

        assert config_path.parent.exists()
        assert backup_dir.exists()
        assert service.config_path == config_path
        assert service.backup_dir == backup_dir

    def test_initialization_with_existing_directories(self, config_service):
        """Test initialization with existing directories works."""
        assert config_service.config_path.parent.exists()
        assert config_service.backup_dir.exists()


class TestConfigServiceLoadSave:
    """Test configuration loading and saving."""

    def test_load_creates_default_config_if_not_exists(self, config_service):
        """Test that load creates default config if file doesn't exist."""
        config = config_service.load_config()

        assert isinstance(config, AppConfig)
        assert config.name == "Aniworld"
        assert config_service.config_path.exists()

    def test_save_and_load_config(self, config_service, sample_config):
        """Test saving and loading configuration."""
        config_service.save_config(sample_config, create_backup=False)

        loaded_config = config_service.load_config()

        assert loaded_config.name == sample_config.name
        assert loaded_config.data_dir == sample_config.data_dir
        assert loaded_config.scheduler.enabled == sample_config.scheduler.enabled
        assert loaded_config.logging.level == sample_config.logging.level
        assert loaded_config.other == sample_config.other

    def test_save_and_load_scheduler_flags_roundtrip(self, config_service):
        """Scheduler auto_download_after_rescan and folder_scan_enabled must
        survive a full save/load roundtrip through ConfigService.

        Regression test for a bug where null legacy alias fields
        (auto_download=None, folder_scan=None) were written to config.json
        on save. On reload the alias mapping was skipped (because the keys
        were present), causing the primary boolean fields to reset to False.
        """
        original = AppConfig(
            scheduler=SchedulerConfig(
                enabled=True,
                auto_download_after_rescan=True,
                folder_scan_enabled=True,
            )
        )
        config_service.save_config(original, create_backup=False)

        # Verify raw JSON does not contain legacy alias keys
        with open(config_service.config_path, "r", encoding="utf-8") as f:
            raw = json.load(f)
        assert "auto_download" not in raw["scheduler"]
        assert "folder_scan" not in raw["scheduler"]
        assert raw["scheduler"]["auto_download_after_rescan"] is True
        assert raw["scheduler"]["folder_scan_enabled"] is True

        # Verify loaded config preserves values
        loaded = config_service.load_config()
        assert loaded.scheduler.auto_download_after_rescan is True
        assert loaded.scheduler.folder_scan_enabled is True

    def test_save_includes_version(self, config_service, sample_config):
        """Test that saved config includes version field."""
        config_service.save_config(sample_config, create_backup=False)

        with open(config_service.config_path, "r", encoding="utf-8") as f:
            data = json.load(f)

        assert "version" in data
        assert data["version"] == ConfigService.CONFIG_VERSION

    def test_save_creates_backup_by_default(self, config_service, sample_config):
        """Test that save creates backup by default if file exists."""
        # Save initial config
        config_service.save_config(sample_config, create_backup=False)

        # Modify and save again (should create backup)
        sample_config.name = "Modified"
        config_service.save_config(sample_config, create_backup=True)

        backups = list(config_service.backup_dir.glob("*.json"))
        assert len(backups) == 1

    def test_save_atomic_operation(self, config_service, sample_config):
        """Test that save is atomic (uses temp file)."""
        # Mock exception during JSON dump by using invalid data
        # This should not corrupt existing config
        config_service.save_config(sample_config, create_backup=False)

        # Verify temp file is cleaned up after successful save
        temp_files = list(config_service.config_path.parent.glob("*.tmp"))
        assert len(temp_files) == 0

    def test_load_invalid_json_raises_error(self, config_service):
        """Test that loading invalid JSON raises ConfigValidationError."""
        # Write invalid JSON
        config_service.config_path.write_text("invalid json {")

        with pytest.raises(ConfigValidationError, match="Invalid JSON"):
            config_service.load_config()


class TestConfigServiceValidation:
    """Test configuration validation."""

    def test_validate_valid_config(self, config_service, sample_config):
        """Test validation of valid configuration."""
        result = config_service.validate_config(sample_config)

        assert result.valid is True
        assert result.errors == []

    def test_validate_invalid_config(self, config_service):
        """Test validation of invalid configuration."""
        # Create config with backups enabled but no path
        invalid_config = AppConfig(
            backup=BackupConfig(enabled=True, path=None)
        )

        result = config_service.validate_config(invalid_config)

        assert result.valid is False
        assert len(result.errors or []) > 0

    def test_save_invalid_config_raises_error(self, config_service):
        """Test that saving invalid config raises error."""
        invalid_config = AppConfig(
            backup=BackupConfig(enabled=True, path=None)
        )

        with pytest.raises(ConfigValidationError, match="Cannot save invalid"):
            config_service.save_config(invalid_config)


class TestConfigServiceUpdate:
    """Test configuration updates."""

    def test_update_config(self, config_service, sample_config):
        """Test updating configuration."""
        config_service.save_config(sample_config, create_backup=False)

        update = ConfigUpdate(
            scheduler=SchedulerConfig(enabled=False, interval_minutes=60),
            logging=LoggingConfig(level="INFO"),
        )

        updated_config = config_service.update_config(update)

        assert updated_config.scheduler.enabled is False
        assert updated_config.scheduler.interval_minutes == 60
        assert updated_config.logging.level == "INFO"
        # Other fields should remain unchanged
        assert updated_config.name == sample_config.name
        assert updated_config.data_dir == sample_config.data_dir

    def test_update_persists_changes(self, config_service, sample_config):
        """Test that updates are persisted to disk."""
        config_service.save_config(sample_config, create_backup=False)

        update = ConfigUpdate(logging=LoggingConfig(level="ERROR"))
        config_service.update_config(update)

        # Load fresh config from disk
        loaded = config_service.load_config()
        assert loaded.logging.level == "ERROR"


class TestConfigServiceBackups:
    """Test configuration backup functionality."""

    def test_create_backup(self, config_service, sample_config):
        """Test creating configuration backup."""
        config_service.save_config(sample_config, create_backup=False)

        backup_path = config_service.create_backup()

        assert backup_path.exists()
        assert backup_path.suffix == ".json"
        assert "config_backup_" in backup_path.name

    def test_create_backup_with_custom_name(
        self, config_service, sample_config
    ):
        """Test creating backup with custom name."""
        config_service.save_config(sample_config, create_backup=False)

        backup_path = config_service.create_backup(name="my_backup")

        assert backup_path.name == "my_backup.json"

    def test_create_backup_without_config_raises_error(self, config_service):
        """Test that creating backup without config file raises error."""
        with pytest.raises(ConfigBackupError, match="Cannot backup non-existent"):
            config_service.create_backup()

    def test_list_backups(self, config_service, sample_config):
        """Test listing configuration backups."""
        config_service.save_config(sample_config, create_backup=False)

        # Create multiple backups
        config_service.create_backup(name="backup1")
        config_service.create_backup(name="backup2")
        config_service.create_backup(name="backup3")

        backups = config_service.list_backups()

        assert len(backups) == 3
        assert all("name" in b for b in backups)
        assert all("size_bytes" in b for b in backups)
        assert all("created_at" in b for b in backups)

        # Should be sorted by creation time (newest first)
        backup_names = [b["name"] for b in backups]
        assert "backup3.json" in backup_names

    def test_list_backups_empty(self, config_service):
        """Test listing backups when none exist."""
        backups = config_service.list_backups()
        assert backups == []

    def test_restore_backup(self, config_service, sample_config):
        """Test restoring configuration from backup."""
        # Save initial config and create backup
        config_service.save_config(sample_config, create_backup=False)
        config_service.create_backup(name="original")

        # Modify and save config
        sample_config.name = "Modified"
        config_service.save_config(sample_config, create_backup=False)

        # Restore from backup
        restored = config_service.restore_backup("original.json")

        assert restored.name == "TestApp"  # Original name

    def test_restore_backup_creates_pre_restore_backup(
        self, config_service, sample_config
    ):
        """Test that restore creates pre-restore backup."""
        config_service.save_config(sample_config, create_backup=False)
        config_service.create_backup(name="backup1")

        sample_config.name = "Modified"
        config_service.save_config(sample_config, create_backup=False)

        config_service.restore_backup("backup1.json")

        backups = config_service.list_backups()
        backup_names = [b["name"] for b in backups]

        assert any("pre_restore" in name for name in backup_names)

    def test_restore_nonexistent_backup_raises_error(self, config_service):
        """Test that restoring non-existent backup raises error."""
        with pytest.raises(ConfigBackupError, match="Backup not found"):
            config_service.restore_backup("nonexistent.json")

    def test_delete_backup(self, config_service, sample_config):
        """Test deleting configuration backup."""
        config_service.save_config(sample_config, create_backup=False)
        config_service.create_backup(name="to_delete")

        config_service.delete_backup("to_delete.json")

        backups = config_service.list_backups()
        assert len(backups) == 0

    def test_delete_nonexistent_backup_raises_error(self, config_service):
        """Test that deleting non-existent backup raises error."""
        with pytest.raises(ConfigBackupError, match="Backup not found"):
            config_service.delete_backup("nonexistent.json")

    def test_cleanup_old_backups(self, config_service, sample_config):
        """Test that old backups are cleaned up when limit exceeded."""
        config_service.save_config(sample_config, create_backup=False)

        # Create more backups than max_backups (3)
        for i in range(5):
            config_service.create_backup(name=f"backup{i}")

        backups = config_service.list_backups()
        assert len(backups) == 3  # Should only keep max_backups


class TestConfigServiceSingleton:
    """Test singleton instance management."""

    def test_get_config_service_returns_singleton(self):
        """Test that get_config_service returns same instance."""
        from src.server.services.config_service import get_config_service

        service1 = get_config_service()
        service2 = get_config_service()

        assert service1 is service2


class TestConfigServiceErrorHandling:
    """Test error handling in ConfigService."""

    def test_save_config_creates_temp_file(
        self, config_service, sample_config
    ):
        """Test that save operation uses temporary file."""
        # Save config and verify temp file is cleaned up
        config_service.save_config(sample_config, create_backup=False)

        # Verify no temp files remain
        temp_files = list(config_service.config_path.parent.glob("*.tmp"))
        assert len(temp_files) == 0

        # Verify config was saved successfully
        loaded = config_service.load_config()
        assert loaded.name == sample_config.name
