"""
Tests for AzureBlobStorageBackend.

TDD Phase 1: RED - Write tests first, then implement to pass.
Uses mocking to avoid requiring actual Azure credentials.
"""

import tempfile
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, PropertyMock, patch

import pytest


@pytest.fixture
def mock_blob_service_client() -> MagicMock:
    """Create a mock BlobServiceClient."""
    return MagicMock()


@pytest.fixture
def mock_container_client(mock_blob_service_client: MagicMock) -> MagicMock:
    """Create a mock ContainerClient."""
    container_client = MagicMock()
    mock_blob_service_client.get_container_client.return_value = container_client
    return container_client


@pytest.fixture
def mock_blob_client(mock_container_client: MagicMock) -> MagicMock:
    """Create a mock BlobClient."""
    blob_client = MagicMock()
    mock_container_client.get_blob_client.return_value = blob_client
    return blob_client


class TestAzureBlobStorageBackendCreation:
    """Tests for AzureBlobStorageBackend instantiation."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_create_with_connection_string(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test creating backend with connection string."""
        from shared.storage.azure import AzureBlobStorageBackend

        connection_string = "DefaultEndpointsProtocol=https;AccountName=test;..."
        backend = AzureBlobStorageBackend(
            connection_string=connection_string,
            container_name="training-images",
        )

        mock_service_class.from_connection_string.assert_called_once_with(
            connection_string
        )
        assert backend.container_name == "training-images"

    @patch("shared.storage.azure.BlobServiceClient")
    def test_create_creates_container_if_not_exists(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that container is created if it doesn't exist."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_container.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="new-container",
            create_container=True,
        )

        mock_container.create_container.assert_called_once()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_create_does_not_create_container_by_default(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that container is not created by default."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_container.exists.return_value = True

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="existing-container",
        )

        mock_container.create_container.assert_not_called()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_is_storage_backend_subclass(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that AzureBlobStorageBackend is a StorageBackend."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import StorageBackend

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        assert isinstance(backend, StorageBackend)


class TestAzureBlobStorageBackendUpload:
    """Tests for AzureBlobStorageBackend.upload method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_file(self, mock_service_class: MagicMock) -> None:
        """Test uploading a file."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as f:
            f.write(b"Hello, World!")
            temp_path = Path(f.name)

        try:
            result = backend.upload(temp_path, "uploads/sample.txt")

            assert result == "uploads/sample.txt"
            mock_container.get_blob_client.assert_called_with("uploads/sample.txt")
            mock_blob.upload_blob.assert_called_once()
        finally:
            temp_path.unlink()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_fails_if_blob_exists_without_overwrite(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that upload fails if blob exists and overwrite is False."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import StorageError

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as f:
            f.write(b"content")
            temp_path = Path(f.name)

        try:
            with pytest.raises(StorageError, match="already exists"):
                backend.upload(temp_path, "existing.txt", overwrite=False)
        finally:
            temp_path.unlink()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_succeeds_with_overwrite(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that upload succeeds with overwrite=True."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.NamedTemporaryFile(delete=False, suffix=".txt") as f:
            f.write(b"content")
            temp_path = Path(f.name)

        try:
            result = backend.upload(temp_path, "existing.txt", overwrite=True)

            assert result == "existing.txt"
            mock_blob.upload_blob.assert_called_once()
            # Check overwrite=True was passed
            call_kwargs = mock_blob.upload_blob.call_args[1]
            assert call_kwargs.get("overwrite") is True
        finally:
            temp_path.unlink()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_nonexistent_file_fails(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that uploading nonexistent file fails."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import FileNotFoundStorageError

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with pytest.raises(FileNotFoundStorageError):
            backend.upload(Path("/nonexistent/file.txt"), "sample.txt")


class TestAzureBlobStorageBackendDownload:
    """Tests for AzureBlobStorageBackend.download method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_file(self, mock_service_class: MagicMock) -> None:
        """Test downloading a file."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        # Mock download_blob to return stream
        mock_stream = MagicMock()
        mock_stream.readall.return_value = b"Hello, World!"
        mock_blob.download_blob.return_value = mock_stream

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            local_path = Path(temp_dir) / "downloaded.txt"
            result = backend.download("remote/sample.txt", local_path)

            assert result == local_path
            assert local_path.exists()
            assert local_path.read_bytes() == b"Hello, World!"

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_creates_parent_directories(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that download creates parent directories."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        mock_stream = MagicMock()
        mock_stream.readall.return_value = b"content"
        mock_blob.download_blob.return_value = mock_stream

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            local_path = Path(temp_dir) / "deep" / "nested" / "downloaded.txt"
            result = backend.download("sample.txt", local_path)

            assert local_path.exists()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_nonexistent_blob_fails(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test that downloading nonexistent blob fails."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import FileNotFoundStorageError

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with pytest.raises(FileNotFoundStorageError, match="nonexistent.txt"):
            backend.download("nonexistent.txt", Path("/tmp/file.txt"))


class TestAzureBlobStorageBackendExists:
    """Tests for AzureBlobStorageBackend.exists method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_exists_returns_true_for_existing_blob(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test exists returns True for existing blob."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        assert backend.exists("existing.txt") is True

    @patch("shared.storage.azure.BlobServiceClient")
    def test_exists_returns_false_for_nonexistent_blob(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test exists returns False for nonexistent blob."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        assert backend.exists("nonexistent.txt") is False


class TestAzureBlobStorageBackendListFiles:
    """Tests for AzureBlobStorageBackend.list_files method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_list_files_empty_container(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test listing files in empty container."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_container.list_blobs.return_value = []

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        assert backend.list_files("") == []

    @patch("shared.storage.azure.BlobServiceClient")
    def test_list_files_returns_all_blobs(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test listing all blobs."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container

        # Create mock blob items
        mock_blob1 = MagicMock()
        mock_blob1.name = "file1.txt"
        mock_blob2 = MagicMock()
        mock_blob2.name = "file2.txt"
        mock_blob3 = MagicMock()
        mock_blob3.name = "subdir/file3.txt"
        mock_container.list_blobs.return_value = [mock_blob1, mock_blob2, mock_blob3]

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        files = backend.list_files("")

        assert len(files) == 3
        assert "file1.txt" in files
        assert "file2.txt" in files
        assert "subdir/file3.txt" in files

    @patch("shared.storage.azure.BlobServiceClient")
    def test_list_files_with_prefix(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test listing files with prefix filter."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container

        mock_blob1 = MagicMock()
        mock_blob1.name = "images/a.png"
        mock_blob2 = MagicMock()
        mock_blob2.name = "images/b.png"
        mock_container.list_blobs.return_value = [mock_blob1, mock_blob2]

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        files = backend.list_files("images/")

        mock_container.list_blobs.assert_called_with(name_starts_with="images/")
        assert len(files) == 2


class TestAzureBlobStorageBackendDelete:
    """Tests for AzureBlobStorageBackend.delete method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_delete_existing_blob(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test deleting an existing blob."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        result = backend.delete("sample.txt")

        assert result is True
        mock_blob.delete_blob.assert_called_once()

    @patch("shared.storage.azure.BlobServiceClient")
    def test_delete_nonexistent_blob_returns_false(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test deleting nonexistent blob returns False."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        result = backend.delete("nonexistent.txt")

        assert result is False
        mock_blob.delete_blob.assert_not_called()


class TestAzureBlobStorageBackendGetUrl:
    """Tests for AzureBlobStorageBackend.get_url method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_get_url_returns_blob_url(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test get_url returns blob URL."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True
        mock_blob.url = "https://account.blob.core.windows.net/container/sample.txt"

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        url = backend.get_url("sample.txt")

        assert url == "https://account.blob.core.windows.net/container/sample.txt"

    @patch("shared.storage.azure.BlobServiceClient")
    def test_get_url_nonexistent_blob_fails(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test get_url for nonexistent blob fails."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import FileNotFoundStorageError

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with pytest.raises(FileNotFoundStorageError):
            backend.get_url("nonexistent.txt")


class TestAzureBlobStorageBackendUploadBytes:
    """Tests for AzureBlobStorageBackend.upload_bytes method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_bytes(self, mock_service_class: MagicMock) -> None:
        """Test uploading bytes directly."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        data = b"Binary content here"
        result = backend.upload_bytes(data, "binary.dat")

        assert result == "binary.dat"
        mock_blob.upload_blob.assert_called_once()


class TestAzureBlobStorageBackendDownloadBytes:
    """Tests for AzureBlobStorageBackend.download_bytes method."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_bytes(self, mock_service_class: MagicMock) -> None:
        """Test downloading blob as bytes."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = True

        mock_stream = MagicMock()
        mock_stream.readall.return_value = b"Hello, World!"
        mock_blob.download_blob.return_value = mock_stream

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        data = backend.download_bytes("sample.txt")

        assert data == b"Hello, World!"

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_bytes_nonexistent(
        self, mock_service_class: MagicMock
    ) -> None:
        """Test downloading nonexistent blob as bytes."""
        from shared.storage.azure import AzureBlobStorageBackend
        from shared.storage.base import FileNotFoundStorageError

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with pytest.raises(FileNotFoundStorageError):
            backend.download_bytes("nonexistent.txt")


class TestAzureBlobStorageBackendBatchOperations:
    """Tests for batch operations in AzureBlobStorageBackend."""

    @patch("shared.storage.azure.BlobServiceClient")
    def test_upload_directory(self, mock_service_class: MagicMock) -> None:
        """Test uploading an entire directory."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container
        mock_blob = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob
        mock_blob.exists.return_value = False

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            temp_path = Path(temp_dir)
            (temp_path / "file1.txt").write_text("content1")
            (temp_path / "subdir").mkdir()
            (temp_path / "subdir" / "file2.txt").write_text("content2")

            results = backend.upload_directory(temp_path, "uploads/")

            assert len(results) == 2
            assert "uploads/file1.txt" in results
            assert "uploads/subdir/file2.txt" in results

    @patch("shared.storage.azure.BlobServiceClient")
    def test_download_directory(self, mock_service_class: MagicMock) -> None:
        """Test downloading blobs matching a prefix."""
        from shared.storage.azure import AzureBlobStorageBackend

        mock_service = MagicMock()
        mock_service_class.from_connection_string.return_value = mock_service
        mock_container = MagicMock()
        mock_service.get_container_client.return_value = mock_container

        # Mock blob listing
        mock_blob1 = MagicMock()
        mock_blob1.name = "images/a.png"
        mock_blob2 = MagicMock()
        mock_blob2.name = "images/b.png"
        mock_container.list_blobs.return_value = [mock_blob1, mock_blob2]

        # Mock blob clients
        mock_blob_client = MagicMock()
        mock_container.get_blob_client.return_value = mock_blob_client
        mock_blob_client.exists.return_value = True
        mock_stream = MagicMock()
        mock_stream.readall.return_value = b"image content"
        mock_blob_client.download_blob.return_value = mock_stream

        backend = AzureBlobStorageBackend(
            connection_string="connection_string",
            container_name="container",
        )

        with tempfile.TemporaryDirectory() as temp_dir:
            local_path = Path(temp_dir)
            results = backend.download_directory("images/", local_path)

            assert len(results) == 2
            # Files should be created relative to prefix
            assert (local_path / "a.png").exists() or (local_path / "images" / "a.png").exists()
