"""Tests for the encrypted storage module."""

from __future__ import annotations

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

import pytest

from gaol.storage.models import (
    EncryptionBackend,
    VolumeConfig,
    VolumeMount,
    VolumeState,
    VolumeStatus,
    VolumesStore,
)
from gaol.storage.exceptions import (
    BackendNotAvailableError,
    InsufficientPermissionsError,
    MountError,
    StorageError,
    VolumeExistsError,
    VolumeKeyError,
    VolumeLockedError,
    VolumeMountedError,
    VolumeNotFoundError,
    VolumeUnlockedError,
)


# =============================================================================
# Model Tests
# =============================================================================


class TestVolumeConfig:
    """Tests for VolumeConfig model."""

    def test_default_values(self) -> None:
        """Test default configuration values."""
        config = VolumeConfig(name="test")
        assert config.backend == EncryptionBackend.LUKS
        assert config.size_mb == 1024
        assert config.filesystem == "ext4"
        assert config.cipher == "aes-xts-plain64"
        assert config.key_size == 256
        assert config.description is None

    def test_gocryptfs_config(self) -> None:
        """Test gocryptfs backend configuration."""
        config = VolumeConfig(
            name="secrets",
            backend=EncryptionBackend.GOCRYPTFS,
            size_mb=512,
        )
        assert config.backend == EncryptionBackend.GOCRYPTFS
        assert config.size_mb == 512

    def test_luks_config(self) -> None:
        """Test LUKS backend configuration."""
        config = VolumeConfig(
            name="data",
            backend=EncryptionBackend.LUKS,
            size_mb=2048,
            filesystem="xfs",
            description="Data volume",
        )
        assert config.backend == EncryptionBackend.LUKS
        assert config.size_mb == 2048
        assert config.filesystem == "xfs"
        assert config.description == "Data volume"

    def test_name_validation(self) -> None:
        """Test volume name validation."""
        # Valid names
        VolumeConfig(name="test")
        VolumeConfig(name="test-volume")
        VolumeConfig(name="test_volume")
        VolumeConfig(name="test.volume")
        VolumeConfig(name="Test123")

        # Invalid names
        with pytest.raises(ValueError):
            VolumeConfig(name="")  # Empty
        with pytest.raises(ValueError):
            VolumeConfig(name="-test")  # Starts with hyphen
        with pytest.raises(ValueError):
            VolumeConfig(name="test volume")  # Contains space

    def test_size_validation(self) -> None:
        """Test volume size validation."""
        # Valid sizes
        VolumeConfig(name="test", size_mb=10)
        VolumeConfig(name="test", size_mb=1024)

        # Invalid sizes
        with pytest.raises(ValueError):
            VolumeConfig(name="test", size_mb=5)  # Too small
        with pytest.raises(ValueError):
            VolumeConfig(name="test", size_mb=0)


class TestVolumeState:
    """Tests for VolumeState model."""

    def test_initial_state(self) -> None:
        """Test initial volume state."""
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            storage_path=Path("/tmp/test.img"),
            size_mb=1024,
            key_secret_name="volume_test_key",
        )
        assert state.status == VolumeStatus.CREATED
        assert state.mount_point is None
        assert state.loop_device is None
        assert state.dm_name is None

    def test_unlocked_state(self) -> None:
        """Test unlocked volume state."""
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.GOCRYPTFS,
            status=VolumeStatus.UNLOCKED,
            storage_path=Path("/tmp/test/cipher"),
            mount_point=Path("/tmp/test/mount"),
            size_mb=512,
            key_secret_name="volume_test_key",
            unlocked_at=datetime.now(),
        )
        assert state.status == VolumeStatus.UNLOCKED
        assert state.mount_point is not None

    def test_luks_specific_fields(self) -> None:
        """Test LUKS-specific state fields."""
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            status=VolumeStatus.UNLOCKED,
            storage_path=Path("/tmp/test.img"),
            mount_point=Path("/tmp/mounts/test"),
            loop_device="/dev/loop0",
            dm_name="gaol-test",
            size_mb=1024,
            filesystem="ext4",
            key_secret_name="volume_test_key",
        )
        assert state.loop_device == "/dev/loop0"
        assert state.dm_name == "gaol-test"
        assert state.filesystem == "ext4"


class TestVolumeMount:
    """Tests for VolumeMount model."""

    def test_default_values(self) -> None:
        """Test default mount configuration."""
        mount = VolumeMount(
            volume_name="mydata",
            container_path="/data",
        )
        assert mount.read_only is False
        assert mount.auto_unlock is True

    def test_readonly_mount(self) -> None:
        """Test read-only mount configuration."""
        mount = VolumeMount(
            volume_name="secrets",
            container_path="/secrets",
            read_only=True,
            auto_unlock=False,
        )
        assert mount.read_only is True
        assert mount.auto_unlock is False


class TestVolumesStore:
    """Tests for VolumesStore model."""

    def test_empty_store(self) -> None:
        """Test empty store."""
        store = VolumesStore()
        assert store.version == 1
        assert store.volumes == {}
        assert store.list_volumes() == []

    def test_add_volume(self) -> None:
        """Test adding a volume."""
        store = VolumesStore()
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            storage_path=Path("/tmp/test.img"),
            size_mb=1024,
            key_secret_name="volume_test_key",
        )
        store.add_volume(state)
        assert "test" in store.volumes
        assert store.get_volume("test") == state

    def test_remove_volume(self) -> None:
        """Test removing a volume."""
        store = VolumesStore()
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            storage_path=Path("/tmp/test.img"),
            size_mb=1024,
            key_secret_name="volume_test_key",
        )
        store.add_volume(state)
        assert store.remove_volume("test") is True
        assert store.get_volume("test") is None
        assert store.remove_volume("nonexistent") is False

    def test_list_volumes(self) -> None:
        """Test listing volumes."""
        store = VolumesStore()
        for name in ["vol1", "vol2", "vol3"]:
            store.add_volume(
                VolumeState(
                    name=name,
                    backend=EncryptionBackend.GOCRYPTFS,
                    storage_path=Path(f"/tmp/{name}"),
                    size_mb=100,
                    key_secret_name=f"volume_{name}_key",
                )
            )
        volumes = store.list_volumes()
        assert len(volumes) == 3
        assert {v.name for v in volumes} == {"vol1", "vol2", "vol3"}


# =============================================================================
# Exception Tests
# =============================================================================


class TestStorageExceptions:
    """Tests for storage exceptions."""

    def test_storage_error(self) -> None:
        """Test base storage error."""
        err = StorageError("test error")
        assert str(err) == "test error"

    def test_volume_not_found_error(self) -> None:
        """Test volume not found error."""
        err = VolumeNotFoundError("myvolume")
        assert err.name == "myvolume"
        assert "myvolume" in str(err)

    def test_volume_exists_error(self) -> None:
        """Test volume exists error."""
        err = VolumeExistsError("myvolume")
        assert err.name == "myvolume"
        assert "already exists" in str(err)

    def test_volume_locked_error(self) -> None:
        """Test volume locked error."""
        err = VolumeLockedError("myvolume")
        assert err.name == "myvolume"
        assert "unlock" in str(err)

    def test_volume_unlocked_error(self) -> None:
        """Test volume unlocked error."""
        err = VolumeUnlockedError("myvolume")
        assert err.name == "myvolume"
        assert "already unlocked" in str(err)

    def test_volume_mounted_error(self) -> None:
        """Test volume mounted error."""
        err = VolumeMountedError("myvolume")
        assert err.name == "myvolume"
        assert "mounted" in str(err)

        err_with_container = VolumeMountedError("myvolume", "mycontainer")
        assert "mycontainer" in str(err_with_container)

    def test_volume_key_error(self) -> None:
        """Test volume key error."""
        err = VolumeKeyError("myvolume", "key not found")
        assert err.name == "myvolume"
        assert err.reason == "key not found"
        assert "key not found" in str(err)

    def test_backend_not_available_error(self) -> None:
        """Test backend not available error."""
        err = BackendNotAvailableError("luks", "cryptsetup not found")
        assert err.backend == "luks"
        assert err.reason == "cryptsetup not found"
        assert "cryptsetup not found" in str(err)

    def test_mount_error(self) -> None:
        """Test mount error."""
        err = MountError("mount", "device busy")
        assert err.operation == "mount"
        assert err.reason == "device busy"
        assert "device busy" in str(err)

    def test_insufficient_permissions_error(self) -> None:
        """Test insufficient permissions error."""
        err = InsufficientPermissionsError("LUKS operations")
        assert err.operation == "LUKS operations"
        assert "sudo" in str(err) or "root" in str(err)


# =============================================================================
# Store Tests
# =============================================================================


class TestVolumeStore:
    """Tests for VolumeStore persistence."""

    @pytest.fixture
    def temp_store(self):
        """Create a temporary store."""
        with tempfile.TemporaryDirectory() as tmpdir:
            from gaol.storage.store import VolumeStore

            yield VolumeStore(Path(tmpdir) / "volumes.yaml")

    def test_save_and_get(self, temp_store) -> None:
        """Test saving and retrieving a volume."""
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            storage_path=Path("/tmp/test.img"),
            size_mb=1024,
            key_secret_name="volume_test_key",
        )

        temp_store.save(state)
        retrieved = temp_store.get("test")

        assert retrieved is not None
        assert retrieved.name == "test"
        assert retrieved.backend == EncryptionBackend.LUKS

    def test_list_empty(self, temp_store) -> None:
        """Test listing empty store."""
        assert temp_store.list() == []

    def test_list_volumes(self, temp_store) -> None:
        """Test listing volumes."""
        for name in ["vol1", "vol2"]:
            temp_store.save(
                VolumeState(
                    name=name,
                    backend=EncryptionBackend.GOCRYPTFS,
                    storage_path=Path(f"/tmp/{name}"),
                    size_mb=100,
                    key_secret_name=f"volume_{name}_key",
                )
            )
        volumes = temp_store.list()
        assert len(volumes) == 2

    def test_delete(self, temp_store) -> None:
        """Test deleting a volume."""
        state = VolumeState(
            name="test",
            backend=EncryptionBackend.LUKS,
            storage_path=Path("/tmp/test.img"),
            size_mb=1024,
            key_secret_name="volume_test_key",
        )

        temp_store.save(state)
        assert temp_store.delete("test") is True
        assert temp_store.get("test") is None

    def test_exists(self, temp_store) -> None:
        """Test checking if volume exists."""
        assert temp_store.exists("test") is False

        temp_store.save(
            VolumeState(
                name="test",
                backend=EncryptionBackend.LUKS,
                storage_path=Path("/tmp/test.img"),
                size_mb=1024,
                key_secret_name="volume_test_key",
            )
        )
        assert temp_store.exists("test") is True


# =============================================================================
# Backend Tests
# =============================================================================


class TestGocryptfsBackend:
    """Tests for gocryptfs backend."""

    def test_name(self) -> None:
        """Test backend name."""
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        backend = GocryptfsBackend()
        assert backend.name == "gocryptfs"

    def test_is_available_true(self) -> None:
        """Test availability check when available."""
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        with patch("shutil.which", return_value="/usr/bin/gocryptfs"):
            backend = GocryptfsBackend()
            assert backend.is_available() is True

    def test_is_available_false(self) -> None:
        """Test availability check when not available."""
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        with patch("shutil.which", return_value=None):
            backend = GocryptfsBackend()
            assert backend.is_available() is False

    def test_create_not_available(self) -> None:
        """Test create fails when gocryptfs not available."""
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        with patch("shutil.which", return_value=None):
            backend = GocryptfsBackend()
            config = VolumeConfig(name="test", backend=EncryptionBackend.GOCRYPTFS)
            with pytest.raises(BackendNotAvailableError):
                backend.create(config, b"key" * 8, Path("/tmp"))


class TestLuksBackend:
    """Tests for LUKS backend."""

    def test_name(self) -> None:
        """Test backend name."""
        from gaol.storage.backends.luks import LuksBackend

        backend = LuksBackend()
        assert backend.name == "luks"

    def test_is_available_true(self) -> None:
        """Test availability check when available."""
        from gaol.storage.backends.luks import LuksBackend

        with patch("shutil.which", return_value="/usr/sbin/cryptsetup"):
            backend = LuksBackend()
            assert backend.is_available() is True

    def test_is_available_false(self) -> None:
        """Test availability check when not available."""
        from gaol.storage.backends.luks import LuksBackend

        with patch("shutil.which", return_value=None):
            backend = LuksBackend()
            assert backend.is_available() is False

    def test_requires_root(self) -> None:
        """Test that LUKS operations require root."""
        from gaol.storage.backends.luks import LuksBackend

        with patch("os.geteuid", return_value=1000):  # Non-root user
            backend = LuksBackend()
            config = VolumeConfig(name="test", backend=EncryptionBackend.LUKS)
            with pytest.raises(InsufficientPermissionsError):
                backend.create(config, b"key" * 8, Path("/tmp"))

    def test_dm_name_generation(self) -> None:
        """Test device mapper name generation."""
        from gaol.storage.backends.luks import LuksBackend

        backend = LuksBackend()
        assert backend._get_dm_name("myvolume") == "gaol-myvolume"


# =============================================================================
# Backend Factory Tests
# =============================================================================


class TestBackendFactory:
    """Tests for backend factory."""

    def test_get_luks_backend(self) -> None:
        """Test getting LUKS backend."""
        from gaol.storage.backends import get_backend
        from gaol.storage.backends.luks import LuksBackend

        backend = get_backend(EncryptionBackend.LUKS)
        assert isinstance(backend, LuksBackend)

    def test_get_gocryptfs_backend(self) -> None:
        """Test getting gocryptfs backend."""
        from gaol.storage.backends import get_backend
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        backend = get_backend(EncryptionBackend.GOCRYPTFS)
        assert isinstance(backend, GocryptfsBackend)


# =============================================================================
# Manager Tests
# =============================================================================


class TestVolumeManager:
    """Tests for VolumeManager."""

    @pytest.fixture
    def mock_manager(self):
        """Create a manager with mocked dependencies."""
        from gaol.storage.manager import VolumeManager
        from gaol.storage.store import VolumeStore

        with tempfile.TemporaryDirectory() as tmpdir:
            store = VolumeStore(Path(tmpdir) / "volumes.yaml")
            manager = VolumeManager(store=store)

            # Mock secrets store
            with patch("gaol.storage.manager.get_secret_store") as mock_secrets:
                mock_store = MagicMock()
                mock_store.create.return_value = None
                mock_store.get.return_value = "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY="  # base64 key
                mock_store.delete.return_value = True
                mock_secrets.return_value = mock_store

                yield manager

    def test_create_volume_exists_error(self, mock_manager) -> None:
        """Test that creating duplicate volume raises error."""
        # Create first volume
        with patch.object(mock_manager, "_get_backend") as mock_backend:
            backend = MagicMock()
            backend.create.return_value = VolumeState(
                name="test",
                backend=EncryptionBackend.GOCRYPTFS,
                storage_path=Path("/tmp/test"),
                size_mb=100,
                key_secret_name="volume_test_key",
            )
            mock_backend.return_value = backend

            mock_manager.create(VolumeConfig(name="test", backend=EncryptionBackend.GOCRYPTFS))

        # Try to create again
        with pytest.raises(VolumeExistsError):
            mock_manager.create(VolumeConfig(name="test"))

    def test_unlock_not_found(self, mock_manager) -> None:
        """Test unlocking non-existent volume."""
        with pytest.raises(VolumeNotFoundError):
            mock_manager.unlock("nonexistent")

    def test_lock_not_found(self, mock_manager) -> None:
        """Test locking non-existent volume."""
        with pytest.raises(VolumeNotFoundError):
            mock_manager.lock("nonexistent")

    def test_destroy_not_found(self, mock_manager) -> None:
        """Test destroying non-existent volume."""
        with pytest.raises(VolumeNotFoundError):
            mock_manager.destroy("nonexistent")

    def test_get_not_found(self, mock_manager) -> None:
        """Test getting non-existent volume."""
        assert mock_manager.get("nonexistent") is None

    def test_list_empty(self, mock_manager) -> None:
        """Test listing empty volumes."""
        assert mock_manager.list() == []

    def test_get_mount_path_not_found(self, mock_manager) -> None:
        """Test getting mount path for non-existent volume."""
        with pytest.raises(VolumeNotFoundError):
            mock_manager.get_mount_path("nonexistent")


# =============================================================================
# Integration Tests (with mocked subprocess)
# =============================================================================


class TestGocryptfsIntegration:
    """Integration tests for gocryptfs backend with mocked subprocess."""

    @pytest.fixture
    def mock_gocryptfs(self):
        """Mock gocryptfs availability and commands."""
        with patch("shutil.which", return_value="/usr/bin/gocryptfs"):
            with patch("subprocess.run") as mock_run:
                mock_run.return_value = MagicMock(returncode=0, stdout=b"", stderr=b"")
                yield mock_run

    def test_create_volume(self, mock_gocryptfs) -> None:
        """Test creating a gocryptfs volume."""
        from gaol.storage.backends.gocryptfs import GocryptfsBackend

        with tempfile.TemporaryDirectory() as tmpdir:
            backend = GocryptfsBackend()
            config = VolumeConfig(
                name="test",
                backend=EncryptionBackend.GOCRYPTFS,
                size_mb=100,
            )
            key = b"0123456789abcdef0123456789abcdef"

            state = backend.create(config, key, Path(tmpdir))

            assert state.name == "test"
            assert state.backend == EncryptionBackend.GOCRYPTFS
            assert state.status == VolumeStatus.LOCKED
            assert mock_gocryptfs.called


class TestLuksIntegration:
    """Integration tests for LUKS backend with mocked subprocess."""

    @pytest.fixture
    def mock_luks(self):
        """Mock LUKS tools and root permissions."""
        with patch("shutil.which", return_value="/usr/sbin/cryptsetup"):
            with patch("os.geteuid", return_value=0):  # Simulate root
                with patch("subprocess.run") as mock_run:
                    mock_run.return_value = MagicMock(
                        returncode=0, stdout=b"/dev/loop0\n", stderr=b""
                    )
                    yield mock_run

    def test_create_volume(self, mock_luks) -> None:
        """Test creating a LUKS volume."""
        from gaol.storage.backends.luks import LuksBackend

        with tempfile.TemporaryDirectory() as tmpdir:
            backend = LuksBackend()
            config = VolumeConfig(
                name="test",
                backend=EncryptionBackend.LUKS,
                size_mb=100,
            )
            key = b"0123456789abcdef0123456789abcdef"

            state = backend.create(config, key, Path(tmpdir))

            assert state.name == "test"
            assert state.backend == EncryptionBackend.LUKS
            assert state.status == VolumeStatus.LOCKED
            assert state.filesystem == "ext4"
            assert mock_luks.called
