"""Tests for gaol migration module."""

from __future__ import annotations

from datetime import datetime
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest.mock import MagicMock, patch

import pytest

from gaol.migration import (
    FeatureTranslator,
    FeatureWarning,
    FeatureWarningSeverity,
    MigrationError,
    MigrationPlan,
    MigrationState,
    MigrationStatus,
    MigrationValidationError,
    Migrator,
    RuntimeCapabilities,
    get_migrator,
    get_runtime_capabilities,
)
from gaol.models.container import (
    ContainerConfig,
    ContainerState,
    ContainerStatus,
    NetworkConfig,
    NetworkMode,
    ResourceLimits,
)


class TestMigrationModels:
    """Tests for migration data models."""

    def test_feature_warning_creation(self) -> None:
        """Test creating a feature warning."""
        warning = FeatureWarning(
            feature="cpu_shares",
            source_value="1024",
            target_behavior="CPU shares not supported; will use vCPU count instead",
            severity=FeatureWarningSeverity.WARNING,
        )
        assert warning.feature == "cpu_shares"
        assert warning.source_value == "1024"
        assert warning.severity == FeatureWarningSeverity.WARNING

    def test_feature_warning_info_severity(self) -> None:
        """Test info severity warning."""
        warning = FeatureWarning(
            feature="filesystem_type",
            target_behavior="Filesystem will be stored as directory",
            severity=FeatureWarningSeverity.INFO,
        )
        assert warning.severity == FeatureWarningSeverity.INFO
        assert warning.source_value is None

    def test_migration_plan_creation(self) -> None:
        """Test creating a migration plan."""
        plan = MigrationPlan(
            container_name="test-container",
            source_runtime="docker",
            target_runtime="nspawn",
            preserve_source=True,
            estimated_size_bytes=1024 * 1024 * 100,  # 100MB
        )
        assert plan.container_name == "test-container"
        assert plan.source_runtime == "docker"
        assert plan.target_runtime == "nspawn"
        assert plan.preserve_source is True
        assert plan.estimated_size_bytes == 1024 * 1024 * 100

    def test_migration_plan_with_warnings(self) -> None:
        """Test migration plan with feature warnings."""
        warnings = [
            FeatureWarning(
                feature="pause",
                target_behavior="Pause/unpause not supported on target runtime",
                severity=FeatureWarningSeverity.INFO,
            ),
        ]
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
            feature_warnings=warnings,
        )
        assert len(plan.feature_warnings) == 1
        assert plan.feature_warnings[0].feature == "pause"

    def test_migration_plan_has_critical_warnings(self) -> None:
        """Test has_critical_warnings property."""
        # Plan without critical warnings
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
            feature_warnings=[
                FeatureWarning(
                    feature="test",
                    target_behavior="test",
                    severity=FeatureWarningSeverity.INFO,
                ),
            ],
        )
        assert plan.has_critical_warnings is False

        # Plan with critical warning
        plan_critical = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
            feature_warnings=[
                FeatureWarning(
                    feature="critical_test",
                    target_behavior="critical",
                    severity=FeatureWarningSeverity.CRITICAL,
                ),
            ],
        )
        assert plan_critical.has_critical_warnings is True

    def test_migration_state_creation(self) -> None:
        """Test creating a migration state."""
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
        )
        state = MigrationState(plan=plan)
        assert state.status == MigrationStatus.PENDING
        assert state.error is None
        assert state.export_path is None

    def test_migration_state_transitions(self) -> None:
        """Test migration state transitions."""
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
        )
        state = MigrationState(plan=plan)

        # Mark started
        state.mark_started()
        assert state.status == MigrationStatus.VALIDATING
        assert state.started_at is not None

        # Mark completed
        state.mark_completed()
        assert state.status == MigrationStatus.COMPLETED
        assert state.completed_at is not None

    def test_migration_state_failure(self) -> None:
        """Test migration state failure handling."""
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
        )
        state = MigrationState(plan=plan)
        state.mark_started()

        state.mark_failed("Export failed", MigrationStatus.EXPORTING)
        assert state.status == MigrationStatus.FAILED
        assert state.error == "Export failed"
        assert state.error_stage == MigrationStatus.EXPORTING

    def test_runtime_capabilities(self) -> None:
        """Test runtime capabilities model."""
        caps = RuntimeCapabilities(
            name="docker",
            supports_pause=True,
            supports_snapshots=True,
            supports_copy=True,
            supports_cpu_shares=True,
            supports_cpu_count=True,
            supports_memory_limit=True,
            supports_privileged=True,
            supports_capabilities=True,
            supports_read_only=True,
            memory_granularity_mb=1,
            filesystem_type="overlay",
        )
        assert caps.name == "docker"
        assert caps.supports_pause is True
        assert caps.memory_granularity_mb == 1


class TestFeatureTranslator:
    """Tests for FeatureTranslator."""

    def test_get_runtime_capabilities(self) -> None:
        """Test getting runtime capabilities."""
        docker_caps = get_runtime_capabilities("docker")
        assert docker_caps.name == "docker"
        assert docker_caps.supports_pause is True

        nspawn_caps = get_runtime_capabilities("nspawn")
        assert nspawn_caps.name == "nspawn"
        assert nspawn_caps.supports_pause is False

        libvirt_caps = get_runtime_capabilities("libvirt")
        assert libvirt_caps.name == "libvirt"
        assert libvirt_caps.supports_cpu_shares is False

    def test_unknown_runtime_capabilities(self) -> None:
        """Test error for unknown runtime."""
        with pytest.raises(ValueError, match="Unknown runtime"):
            get_runtime_capabilities("unknown")

    def test_translator_initialization(self) -> None:
        """Test translator creation."""
        translator = FeatureTranslator()
        assert translator is not None

    def test_migration_supported_check(self) -> None:
        """Test checking if migration is supported."""
        translator = FeatureTranslator()

        # Same runtime not supported
        supported, reason = translator.is_migration_supported("docker", "docker")
        assert supported is False
        assert "same" in reason.lower()

        # Unknown runtime not supported
        supported, reason = translator.is_migration_supported("unknown", "docker")
        assert supported is False

        # Valid migrations
        supported, reason = translator.is_migration_supported("docker", "nspawn")
        assert supported is True
        assert reason is None

    def test_analyze_migration_docker_to_nspawn(self) -> None:
        """Test analyzing docker to nspawn migration."""
        translator = FeatureTranslator()
        config = ContainerConfig(name="test")

        translated, warnings = translator.analyze_migration(
            config, "docker", "nspawn"
        )

        assert translated.runtime == "nspawn"
        # Should have warnings about pause not being supported
        pause_warnings = [w for w in warnings if w.feature == "pause"]
        assert len(pause_warnings) == 1

    def test_analyze_migration_read_only_warning(self) -> None:
        """Test warning for read-only root on unsupported runtime."""
        translator = FeatureTranslator()
        config = ContainerConfig(name="test", read_only=True)

        translated, warnings = translator.analyze_migration(
            config, "docker", "nspawn"
        )

        # nspawn doesn't support read-only root
        read_only_warnings = [w for w in warnings if w.feature == "read_only"]
        assert len(read_only_warnings) == 1
        assert translated.read_only is False

    def test_analyze_migration_cpu_shares_warning(self) -> None:
        """Test warning for CPU shares on libvirt."""
        translator = FeatureTranslator()
        config = ContainerConfig(
            name="test",
            resources=ResourceLimits(cpu_shares=1024),
        )

        translated, warnings = translator.analyze_migration(
            config, "docker", "libvirt"
        )

        # libvirt doesn't support CPU shares
        cpu_warnings = [w for w in warnings if w.feature == "cpu_shares"]
        assert len(cpu_warnings) == 1
        assert translated.resources.cpu_shares is None

    def test_analyze_migration_memory_granularity(self) -> None:
        """Test memory rounding for different granularity."""
        translator = FeatureTranslator()
        config = ContainerConfig(
            name="test",
            resources=ResourceLimits(memory_mb=1025),  # Not divisible by 4
        )

        translated, warnings = translator.analyze_migration(
            config, "docker", "libvirt"
        )

        # libvirt has 4MB granularity, should round up
        memory_warnings = [w for w in warnings if w.feature == "memory_limit"]
        assert len(memory_warnings) == 1
        assert translated.resources.memory_mb == 1028  # Rounded to 4MB

    def test_analyze_migration_privileged_warning(self) -> None:
        """Test privileged mode warning for VMs."""
        translator = FeatureTranslator()
        config = ContainerConfig(name="test", privileged=True)

        translated, warnings = translator.analyze_migration(
            config, "docker", "libvirt"
        )

        # VMs don't need privileged mode
        priv_warnings = [w for w in warnings if w.feature == "privileged"]
        assert len(priv_warnings) == 1
        assert translated.privileged is False

    def test_analyze_migration_filesystem_type_info(self) -> None:
        """Test filesystem type change info."""
        translator = FeatureTranslator()
        config = ContainerConfig(name="test")

        translated, warnings = translator.analyze_migration(
            config, "docker", "nspawn"
        )

        # Different filesystem types should generate info
        fs_warnings = [w for w in warnings if w.feature == "filesystem_type"]
        assert len(fs_warnings) == 1
        assert fs_warnings[0].severity == FeatureWarningSeverity.INFO


class TestMigrator:
    """Tests for Migrator class."""

    @pytest.fixture
    def mock_store(self):
        """Create a mock container store."""
        store = MagicMock()
        return store

    @pytest.fixture
    def mock_runtime(self):
        """Create a mock runtime."""
        runtime = MagicMock()
        runtime.is_available.return_value = True
        runtime.name = "nspawn"
        return runtime

    def test_migrator_creation(self) -> None:
        """Test creating a migrator."""
        migrator = Migrator()
        assert migrator is not None

    def test_get_migrator_singleton(self) -> None:
        """Test get_migrator returns singleton."""
        m1 = get_migrator()
        m2 = get_migrator()
        assert m1 is m2

    def test_create_plan_container_not_found(self, mock_store) -> None:
        """Test error when container not found."""
        mock_store.get.return_value = None
        migrator = Migrator(store=mock_store)

        with pytest.raises(MigrationValidationError, match="not found"):
            migrator.create_plan("nonexistent", "nspawn")

    def test_create_plan_same_runtime(self, mock_store) -> None:
        """Test error when migrating to same runtime."""
        stored = MagicMock()
        stored.runtime = "docker"
        stored.config = ContainerConfig(name="test")
        mock_store.get.return_value = stored

        migrator = Migrator(store=mock_store)

        with pytest.raises(MigrationValidationError, match="same"):
            migrator.create_plan("test", "docker")

    @patch("gaol.migration.migrator.get_runtime")
    def test_create_plan_target_unavailable(
        self, mock_get_runtime, mock_store
    ) -> None:
        """Test error when target runtime unavailable."""
        stored = MagicMock()
        stored.runtime = "docker"
        stored.config = ContainerConfig(name="test")
        mock_store.get.return_value = stored

        mock_runtime = MagicMock()
        mock_runtime.is_available.return_value = False
        mock_get_runtime.return_value = mock_runtime

        migrator = Migrator(store=mock_store)

        with pytest.raises(MigrationValidationError, match="not available"):
            migrator.create_plan("test", "nspawn")

    @patch("gaol.migration.migrator.get_runtime")
    def test_create_plan_success(self, mock_get_runtime, mock_store) -> None:
        """Test successful plan creation."""
        stored = MagicMock()
        stored.runtime = "docker"
        stored.config = ContainerConfig(name="test")
        stored.container_id = "abc123"
        mock_store.get.return_value = stored

        mock_runtime = MagicMock()
        mock_runtime.is_available.return_value = True
        mock_runtime.get_filesystem_size.return_value = 1024 * 1024 * 50
        mock_get_runtime.return_value = mock_runtime

        migrator = Migrator(store=mock_store)
        plan = migrator.create_plan("test", "nspawn")

        assert plan.container_name == "test"
        assert plan.source_runtime == "docker"
        assert plan.target_runtime == "nspawn"
        assert plan.estimated_size_bytes == 1024 * 1024 * 50

    def test_execute_dry_run(self, mock_store) -> None:
        """Test dry run execution."""
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
            dry_run=True,
        )

        migrator = Migrator(store=mock_store)
        state = migrator.execute(plan)

        assert state.status == MigrationStatus.COMPLETED

    def test_execute_container_not_found(self, mock_store) -> None:
        """Test execution fails when container not in store."""
        plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
        )

        mock_store.get.return_value = None
        migrator = Migrator(store=mock_store)
        state = migrator.execute(plan)

        assert state.status == MigrationStatus.FAILED
        assert "not found" in state.error.lower()


class TestMigrationExceptions:
    """Tests for migration exceptions."""

    def test_migration_error(self) -> None:
        """Test base migration error."""
        error = MigrationError("Something went wrong")
        assert str(error) == "Something went wrong"

    def test_migration_validation_error(self) -> None:
        """Test validation error."""
        error = MigrationValidationError("Container not found")
        assert str(error) == "Container not found"
        assert isinstance(error, MigrationError)

    def test_migration_rollback_error(self) -> None:
        """Test rollback error with original and rollback errors."""
        from gaol.migration import MigrationRollbackError

        original = Exception("Import failed")
        rollback = Exception("Cleanup failed")
        error = MigrationRollbackError(original, rollback)

        assert error.original_error is original
        assert error.rollback_error is rollback
        assert "Import failed" in str(error)
        assert "Cleanup failed" in str(error)


class TestRuntimeExportImport:
    """Tests for runtime export/import methods."""

    def test_base_runtime_export_not_implemented(self) -> None:
        """Test that base runtime raises NotImplementedError."""
        from gaol.runtimes.base import ContainerRuntime

        # Create a minimal concrete class
        class MinimalRuntime(ContainerRuntime):
            @property
            def name(self) -> str:
                return "minimal"

            def is_available(self) -> bool:
                return True

            def create(self, config):
                pass

            def start(self, container_id):
                pass

            def stop(self, container_id, timeout=10):
                pass

            def destroy(self, container_id, force=False):
                pass

            def exec(self, container_id, command, **kwargs):
                pass

            def list(self, all=False):
                return []

            def status(self, container_id):
                pass

            def logs(self, container_id, **kwargs):
                return ""

        runtime = MinimalRuntime()

        with pytest.raises(NotImplementedError):
            runtime.export_filesystem("test", "/tmp")

        with pytest.raises(NotImplementedError):
            runtime.import_filesystem(ContainerConfig(name="test"), "/tmp/test.tar")

        with pytest.raises(NotImplementedError):
            runtime.get_filesystem_size("test")


class TestCLIMigrate:
    """Tests for CLI migrate command."""

    def test_migrate_command_help(self) -> None:
        """Test migrate command is registered and has help."""
        from typer.testing import CliRunner

        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["migrate", "--help"])

        assert result.exit_code == 0
        assert "migrate" in result.output.lower()
        assert "--to" in result.output
        assert "--dry-run" in result.output

    def test_migrate_missing_target(self) -> None:
        """Test migrate requires --to option."""
        from typer.testing import CliRunner

        from gaol.cli import app

        runner = CliRunner()
        result = runner.invoke(app, ["migrate", "test-container"])

        assert result.exit_code != 0
        assert "--to" in result.output or "Missing" in result.output

    @patch("gaol.migration.migrator.get_migrator")
    @patch("gaol.migration.get_migrator")
    def test_migrate_container_not_found(
        self, mock_get_migrator, mock_get_migrator2
    ) -> None:
        """Test migrate with nonexistent container."""
        from typer.testing import CliRunner

        from gaol.cli import app

        mock_migrator = MagicMock()
        mock_migrator.create_plan.side_effect = MigrationValidationError(
            "Container not found: nonexistent"
        )
        mock_get_migrator.return_value = mock_migrator
        mock_get_migrator2.return_value = mock_migrator

        runner = CliRunner()
        result = runner.invoke(app, ["migrate", "nonexistent", "--to", "nspawn"])

        assert result.exit_code == 1
        assert "not found" in result.output.lower() or "validation failed" in result.output.lower()

    @patch("gaol.migration.migrator.get_migrator")
    @patch("gaol.migration.get_migrator")
    def test_migrate_dry_run(self, mock_get_migrator, mock_get_migrator2) -> None:
        """Test migrate with --dry-run."""
        from typer.testing import CliRunner

        from gaol.cli import app

        mock_migrator = MagicMock()
        mock_plan = MigrationPlan(
            container_name="test",
            source_runtime="docker",
            target_runtime="nspawn",
            feature_warnings=[],
        )
        mock_migrator.create_plan.return_value = mock_plan
        mock_get_migrator.return_value = mock_migrator
        mock_get_migrator2.return_value = mock_migrator

        runner = CliRunner()
        result = runner.invoke(app, ["migrate", "test", "--to", "nspawn", "--dry-run"])

        assert result.exit_code == 0
        assert "dry run" in result.output.lower()
        # execute should not be called for dry run
        mock_migrator.execute.assert_not_called()
