"""
Tests for Object Storage Module

Tests the MinIO/S3-compatible storage functionality.
"""

import asyncio
from datetime import datetime
from unittest.mock import MagicMock, patch

import pytest

from verlex_server.gateway.backend.object_storage import (
    LocalStorageBackend,
    MinIOBackend,
    ObjectMetadata,
    ObjectStorage,
    StorageConfig,
    StorageProvider,
)


class TestStorageConfig:
    """Tests for StorageConfig."""

    def test_default_values(self):
        """Test default configuration values."""
        config = StorageConfig()

        assert config.provider == StorageProvider.MINIO
        assert config.endpoint_url == "http://localhost:9000"
        assert config.access_key == ""  # Empty by default for security
        assert config.secret_key == ""  # Empty by default for security
        assert config.checkpoint_bucket == "gateway-checkpoints"
        assert config.artifact_bucket == "gateway-artifacts"
        assert config.result_bucket == "gateway-results"

    def test_from_env(self):
        """Test creating config from environment variables."""
        with patch.dict('os.environ', {
            'GATEWAY_STORAGE_PROVIDER': 'minio',
            'GATEWAY_STORAGE_ENDPOINT': 'http://custom:9000',
            'GATEWAY_STORAGE_ACCESS_KEY': 'mykey',
            'GATEWAY_STORAGE_SECRET_KEY': 'mysecret',
        }):
            config = StorageConfig.from_env()

            assert config.provider == StorageProvider.MINIO
            assert config.endpoint_url == 'http://custom:9000'
            assert config.access_key == 'mykey'
            assert config.secret_key == 'mysecret'


class TestObjectMetadata:
    """Tests for ObjectMetadata."""

    def test_to_dict(self):
        """Test converting metadata to dictionary."""
        meta = ObjectMetadata(
            key="test/file.txt",
            size_bytes=1024,
            etag="abc123",
            content_type="text/plain",
            last_modified=datetime(2026, 1, 9, 12, 0, 0),
            checksum_sha256="sha256hash",
            custom_metadata={"job_id": "job-123"},
        )

        result = meta.to_dict()

        assert result["key"] == "test/file.txt"
        assert result["size_bytes"] == 1024
        assert result["etag"] == "abc123"
        assert result["custom_metadata"]["job_id"] == "job-123"


class TestLocalStorageBackend:
    """Tests for LocalStorageBackend."""

    @pytest.fixture
    def config(self, tmp_path):
        """Create config with temp directory."""
        return StorageConfig(
            provider=StorageProvider.LOCAL,
            local_storage_path=str(tmp_path / "storage"),
        )

    @pytest.fixture
    def backend(self, config):
        """Create local storage backend."""
        return LocalStorageBackend(config)

    @pytest.mark.asyncio
    async def test_create_bucket(self, backend):
        """Test bucket creation."""
        result = await backend.create_bucket("test-bucket")
        assert result is True

        exists = await backend.bucket_exists("test-bucket")
        assert exists is True

    @pytest.mark.asyncio
    async def test_put_and_get_object(self, backend):
        """Test uploading and downloading objects."""
        await backend.create_bucket("test-bucket")

        # Put object
        data = b"Hello, World!"
        meta = await backend.put_object(
            bucket="test-bucket",
            key="test.txt",
            data=data,
            content_type="text/plain",
        )

        assert meta.key == "test.txt"
        assert meta.size_bytes == len(data)

        # Get object
        retrieved = await backend.get_object("test-bucket", "test.txt")
        assert retrieved == data

    @pytest.mark.asyncio
    async def test_delete_object(self, backend):
        """Test object deletion."""
        await backend.create_bucket("test-bucket")
        await backend.put_object("test-bucket", "delete-me.txt", b"data")

        # Verify exists
        assert await backend.object_exists("test-bucket", "delete-me.txt") is True

        # Delete
        result = await backend.delete_object("test-bucket", "delete-me.txt")
        assert result is True

        # Verify gone
        assert await backend.object_exists("test-bucket", "delete-me.txt") is False

    @pytest.mark.asyncio
    async def test_list_objects(self, backend):
        """Test listing objects."""
        await backend.create_bucket("test-bucket")

        # Add multiple objects
        for i in range(5):
            await backend.put_object("test-bucket", f"file{i}.txt", f"data{i}".encode())

        # List all
        objects = await backend.list_objects("test-bucket")
        assert len(objects) == 5

    @pytest.mark.asyncio
    async def test_object_metadata(self, backend):
        """Test getting object metadata."""
        await backend.create_bucket("test-bucket")

        data = b"test data for metadata"
        await backend.put_object(
            "test-bucket",
            "meta-test.txt",
            data,
            metadata={"custom": "value"},
        )

        meta = await backend.get_object_metadata("test-bucket", "meta-test.txt")

        assert meta is not None
        assert meta.key == "meta-test.txt"
        assert meta.size_bytes == len(data)
        assert meta.custom_metadata.get("custom") == "value"


class TestObjectStorage:
    """Tests for ObjectStorage main class."""

    @pytest.fixture
    def config(self, tmp_path):
        """Create config with local storage."""
        return StorageConfig(
            provider=StorageProvider.LOCAL,
            local_storage_path=str(tmp_path / "storage"),
        )

    @pytest.fixture
    def storage(self, config):
        """Create object storage instance."""
        return ObjectStorage(config)

    @pytest.mark.asyncio
    async def test_initialize(self, storage):
        """Test storage initialization."""
        result = await storage.initialize()
        assert result is True
        assert storage._initialized is True

    @pytest.mark.asyncio
    async def test_save_and_load_checkpoint(self, storage):
        """Test checkpoint save and load."""
        await storage.initialize()

        job_id = "job-123"
        checkpoint_data = b"model state data here"

        # Save checkpoint
        meta = await storage.save_checkpoint(
            job_id=job_id,
            data=checkpoint_data,
            epoch=5,
            step=1000,
        )

        assert meta.key.startswith(f"{job_id}/checkpoint_")
        assert "0005" in meta.key  # epoch
        assert "00001000" in meta.key  # step

        # Load checkpoint
        loaded = await storage.load_checkpoint(job_id)
        assert loaded == checkpoint_data

    @pytest.mark.asyncio
    async def test_load_specific_checkpoint(self, storage):
        """Test loading checkpoint by epoch."""
        await storage.initialize()

        job_id = "job-456"

        # Save multiple checkpoints
        await storage.save_checkpoint(job_id, b"epoch1", epoch=1, step=100)
        await storage.save_checkpoint(job_id, b"epoch2", epoch=2, step=200)
        await storage.save_checkpoint(job_id, b"epoch3", epoch=3, step=300)

        # Load specific epoch
        loaded = await storage.load_checkpoint(job_id, epoch=2)
        assert loaded == b"epoch2"

    @pytest.mark.asyncio
    async def test_list_checkpoints(self, storage):
        """Test listing checkpoints for a job."""
        await storage.initialize()

        job_id = "job-789"

        # Save multiple checkpoints
        for i in range(3):
            await storage.save_checkpoint(job_id, f"data{i}".encode(), epoch=i)

        checkpoints = await storage.list_checkpoints(job_id)
        assert len(checkpoints) == 3

    @pytest.mark.asyncio
    async def test_cleanup_old_checkpoints(self, storage):
        """Test cleaning up old checkpoints."""
        await storage.initialize()

        job_id = "job-cleanup"

        # Save 5 checkpoints
        for i in range(5):
            await storage.save_checkpoint(job_id, f"data{i}".encode(), epoch=i)
            await asyncio.sleep(0.01)  # Ensure different timestamps

        # Keep only 2
        deleted = await storage.cleanup_old_checkpoints(job_id, keep_last=2)
        assert deleted == 3

        # Verify only 2 remain
        remaining = await storage.list_checkpoints(job_id)
        assert len(remaining) == 2

    @pytest.mark.asyncio
    async def test_save_and_load_artifact(self, storage):
        """Test artifact save and load."""
        await storage.initialize()

        job_id = "job-artifact"
        code = b"def main(): pass"

        # Save artifact
        meta = await storage.save_artifact(
            job_id=job_id,
            data=code,
            artifact_type="code",
            filename="main.py",
        )

        assert meta.key == f"{job_id}/artifacts/main.py"

        # Load artifact
        loaded = await storage.load_artifact(job_id, "main.py")
        assert loaded == code

    @pytest.mark.asyncio
    async def test_save_and_load_result(self, storage):
        """Test result save and load."""
        await storage.initialize()

        job_id = "job-result"
        result_data = b"serialized result"

        # Save result
        meta = await storage.save_result(job_id, result_data)
        assert meta.key == f"{job_id}/result"

        # Load result
        loaded = await storage.load_result(job_id)
        assert loaded == result_data

    @pytest.mark.asyncio
    async def test_cleanup_job(self, storage):
        """Test cleaning up all job storage."""
        await storage.initialize()

        job_id = "job-cleanup-all"

        # Save various items
        await storage.save_checkpoint(job_id, b"ckpt")
        await storage.save_artifact(job_id, b"code")
        await storage.save_result(job_id, b"result")

        # Cleanup
        deleted = await storage.cleanup_job(job_id)

        assert deleted["checkpoints"] == 1
        assert deleted["artifacts"] == 1
        assert deleted["results"] == 1

        # Verify all gone
        assert await storage.load_checkpoint(job_id) is None
        assert await storage.load_result(job_id) is None


class TestMinIOBackend:
    """Tests for MinIO backend (mocked)."""

    @pytest.fixture
    def config(self):
        """Create MinIO config."""
        return StorageConfig(
            provider=StorageProvider.MINIO,
            endpoint_url="http://localhost:9000",
        )

    def test_init(self, config):
        """Test MinIO backend initialization."""
        backend = MinIOBackend(config)
        assert backend.config == config
        assert backend._client is None  # Lazy initialization

    @pytest.mark.asyncio
    async def test_put_object_mocked(self, config):
        """Test put_object with mocked boto3."""
        backend = MinIOBackend(config)

        # Mock the boto3 client
        mock_client = MagicMock()
        mock_client.put_object.return_value = {"ETag": '"abc123"'}
        backend._client = mock_client

        result = await backend.put_object(
            bucket="test-bucket",
            key="test.txt",
            data=b"test data",
        )

        assert result.key == "test.txt"
        assert result.etag == "abc123"
        mock_client.put_object.assert_called_once()

    @pytest.mark.asyncio
    async def test_get_object_mocked(self, config):
        """Test get_object with mocked boto3."""
        backend = MinIOBackend(config)

        # Mock the boto3 client
        mock_body = MagicMock()
        mock_body.read.return_value = b"test data"

        mock_client = MagicMock()
        mock_client.get_object.return_value = {"Body": mock_body}
        backend._client = mock_client

        result = await backend.get_object("test-bucket", "test.txt")

        assert result == b"test data"
        mock_client.get_object.assert_called_once_with(Bucket="test-bucket", Key="test.txt")
