"""
Tests for file storage system.

Tests local storage, metadata store, validation, and file service.
"""

import os
from datetime import UTC, datetime
from io import BytesIO
from typing import Any
from uuid import UUID, uuid4

import pytest

from dazzle_back.runtime.file_storage import (
    FileMetadata,
    FileMetadataStore,
    FileValidationError,
    FileValidator,
    LocalStorageBackend,
    create_local_file_service,
    secure_filename,
)

# =============================================================================
# Fixtures
# =============================================================================


@pytest.fixture
def temp_storage_path(tmp_path: Any) -> Any:
    """Create a temporary storage path."""
    storage_path = tmp_path / "uploads"
    storage_path.mkdir()
    return storage_path


@pytest.fixture
def temp_db_path(tmp_path: Any) -> Any:
    """Create a temporary database path."""
    return tmp_path / "files.db"


@pytest.fixture
def local_storage(temp_storage_path: Any) -> Any:
    """Create a local storage backend."""
    return LocalStorageBackend(
        base_path=temp_storage_path,
        base_url="/files",
    )


@pytest.fixture
def metadata_store() -> Any:
    """Create a file metadata store with clean table."""
    database_url = os.environ.get("DATABASE_URL")
    if not database_url:
        pytest.skip("DATABASE_URL not set")
    store = FileMetadataStore(database_url=database_url)
    # Clean table before each test for isolation
    from dazzle_back.runtime.pg_backend import PostgresBackend

    try:
        db = PostgresBackend(database_url)
        with db.connection() as conn:
            conn.execute("DELETE FROM dazzle_files")
    except Exception:
        pass
    return store


@pytest.fixture
def file_service(temp_storage_path: Any) -> Any:
    """Create a file service with clean table."""
    database_url = os.environ.get("DATABASE_URL")
    if not database_url:
        pytest.skip("DATABASE_URL not set")
    # Clean table before each test for isolation
    from dazzle_back.runtime.pg_backend import PostgresBackend

    try:
        db = PostgresBackend(database_url)
        with db.connection() as conn:
            conn.execute("DELETE FROM dazzle_files")
    except Exception:
        pass
    return create_local_file_service(
        base_path=temp_storage_path,
        database_url=database_url,
    )


@pytest.fixture
def sample_file() -> Any:
    """Create a sample file-like object."""
    content = b"Hello, World! This is a test file."
    return BytesIO(content)


@pytest.fixture
def sample_image() -> Any:
    """Create a minimal valid PNG image."""
    # Minimal 1x1 red PNG
    png_data = (
        b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
        b"\x08\x02\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx\x9cc\xf8\xcf\xc0"
        b"\x00\x00\x00\x03\x00\x01\x00\x05\xfe\xd4\x00\x00\x00\x00IEND\xaeB`\x82"
    )
    return BytesIO(png_data)


@pytest.fixture
def sample_pdf() -> Any:
    """Create a minimal valid PDF."""
    pdf_data = b"%PDF-1.4\n1 0 obj\n<<>>\nendobj\ntrailer\n<<>>\n%%EOF"
    return BytesIO(pdf_data)


# =============================================================================
# Secure Filename Tests
# =============================================================================


class TestSecureFilename:
    """Tests for secure_filename function."""

    def test_basic_filename(self) -> None:
        """Test basic filename passes through."""
        assert secure_filename("document.pdf") == "document.pdf"

    def test_removes_path(self) -> None:
        """Test path components are removed."""
        assert secure_filename("/path/to/document.pdf") == "document.pdf"
        # Path.name returns only the final component
        assert secure_filename("../../../etc/passwd") == "passwd"

    def test_replaces_special_chars(self) -> None:
        """Test special characters are replaced."""
        assert secure_filename("my file (1).pdf") == "my_file__1_.pdf"

    def test_removes_leading_dot(self) -> None:
        """Test leading dots are removed."""
        assert secure_filename(".htaccess") == "htaccess"

    def test_limits_length(self) -> None:
        """Test filename length is limited."""
        long_name = "a" * 200 + ".pdf"
        result = secure_filename(long_name)
        assert len(result) <= 114  # 100 + "." + 10 (extension limit)

    def test_empty_becomes_unnamed(self) -> None:
        """Test empty filename becomes unnamed."""
        assert secure_filename("") == "unnamed_file"
        assert secure_filename("...") == "unnamed_file"


# =============================================================================
# FileValidator Tests
# =============================================================================


class TestFileValidator:
    """Tests for FileValidator."""

    def test_validate_valid_file(self, sample_file: Any) -> None:
        """Test validation passes for valid file."""
        validator = FileValidator(max_size=1024 * 1024)
        is_valid, error, content_type = validator.validate(sample_file, "test.txt")

        assert is_valid is True
        assert error is None

    def test_validate_file_too_large(self) -> None:
        """Test validation fails for oversized file."""
        validator = FileValidator(max_size=10)
        large_file = BytesIO(b"x" * 100)

        is_valid, error, _ = validator.validate(large_file, "large.txt")

        assert is_valid is False
        assert error is not None and "exceeds maximum size" in error

    def test_validate_empty_file(self) -> None:
        """Test validation fails for empty file."""
        validator = FileValidator()
        empty_file = BytesIO(b"")

        is_valid, error, _ = validator.validate(empty_file, "empty.txt")

        assert is_valid is False
        assert error is not None and "empty" in error.lower()

    def test_validate_allowed_types(self, sample_image: Any) -> None:
        """Test validation with allowed types."""
        validator = FileValidator(allowed_types=["image/*"])

        is_valid, error, content_type = validator.validate(sample_image, "image.png")

        assert is_valid is True
        assert "image" in content_type

    def test_validate_disallowed_type(self, sample_file: Any) -> None:
        """Test validation fails for disallowed type."""
        validator = FileValidator(allowed_types=["image/*"])

        is_valid, error, _ = validator.validate(sample_file, "test.txt")

        assert is_valid is False
        assert error is not None and "not allowed" in error


# =============================================================================
# LocalStorageBackend Tests
# =============================================================================


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

    @pytest.mark.asyncio
    async def test_store_file(self, local_storage: Any, sample_file: Any) -> None:
        """Test storing a file."""
        metadata = await local_storage.store(
            sample_file,
            "test.txt",
            "text/plain",
        )

        assert metadata.filename == "test.txt"
        assert metadata.content_type == "text/plain"
        assert metadata.size > 0
        assert metadata.url.startswith("/files/")
        assert isinstance(metadata.id, UUID)

    @pytest.mark.asyncio
    async def test_store_with_prefix(self, local_storage: Any, sample_file: Any) -> None:
        """Test storing with path prefix."""
        metadata = await local_storage.store(
            sample_file,
            "test.txt",
            "text/plain",
            path_prefix="documents",
        )

        assert "documents" in metadata.url

    @pytest.mark.asyncio
    async def test_retrieve_file(self, local_storage: Any, sample_file: Any) -> None:
        """Test retrieving a stored file."""
        # Store first
        metadata = await local_storage.store(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # Retrieve
        content = await local_storage.retrieve(metadata.storage_key)

        assert content == b"Hello, World! This is a test file."

    @pytest.mark.asyncio
    async def test_retrieve_not_found(self, local_storage: Any) -> None:
        """Test retrieving non-existent file."""
        with pytest.raises(FileNotFoundError):
            await local_storage.retrieve("nonexistent/file.txt")

    @pytest.mark.asyncio
    async def test_delete_file(
        self, local_storage: Any, sample_file: Any, temp_storage_path: Any
    ) -> None:
        """Test deleting a file."""
        # Store first
        metadata = await local_storage.store(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # File exists
        full_path = temp_storage_path / metadata.storage_key
        assert full_path.exists()

        # Delete
        deleted = await local_storage.delete(metadata.storage_key)

        assert deleted is True
        assert not full_path.exists()

    @pytest.mark.asyncio
    async def test_delete_not_found(self, local_storage: Any) -> None:
        """Test deleting non-existent file."""
        deleted = await local_storage.delete("nonexistent/file.txt")
        assert deleted is False

    @pytest.mark.asyncio
    async def test_stream_file(self, local_storage: Any, sample_file: Any) -> None:
        """Test streaming a file."""
        # Store first
        metadata = await local_storage.store(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # Stream
        chunks = []
        async for chunk in local_storage.stream(metadata.storage_key):
            chunks.append(chunk)

        content = b"".join(chunks)
        assert content == b"Hello, World! This is a test file."

    def test_get_url(self, local_storage: Any) -> None:
        """Test URL generation."""
        url = local_storage.get_url("2025/11/28/abc_test.txt")
        assert url == "/files/2025/11/28/abc_test.txt"


# =============================================================================
# FileMetadataStore Tests
# =============================================================================


@pytest.mark.skipif(
    not os.environ.get("DATABASE_URL"),
    reason="DATABASE_URL not set — FileMetadataStore requires PostgreSQL",
)
class TestFileMetadataStore:
    """Tests for FileMetadataStore."""

    def test_save_and_get(self, metadata_store: Any) -> None:
        """Test saving and retrieving metadata."""

        metadata = FileMetadata(
            id=uuid4(),
            filename="test.txt",
            content_type="text/plain",
            size=100,
            storage_key="2025/11/28/abc_test.txt",
            storage_backend="local",
            created_at=datetime.now(UTC),
            url="/files/2025/11/28/abc_test.txt",
        )

        metadata_store.save(metadata)
        retrieved = metadata_store.get(metadata.id)

        assert retrieved is not None
        assert retrieved.id == metadata.id
        assert retrieved.filename == "test.txt"

    def test_get_not_found(self, metadata_store: Any) -> None:
        """Test getting non-existent metadata."""
        result = metadata_store.get(uuid4())
        assert result is None

    def test_delete(self, metadata_store: Any) -> None:
        """Test deleting metadata."""

        metadata = FileMetadata(
            id=uuid4(),
            filename="test.txt",
            content_type="text/plain",
            size=100,
            storage_key="test.txt",
            storage_backend="local",
            created_at=datetime.now(UTC),
            url="/files/test.txt",
        )

        metadata_store.save(metadata)
        deleted = metadata_store.delete(metadata.id)

        assert deleted is True
        assert metadata_store.get(metadata.id) is None

    def test_get_by_entity(self, metadata_store: Any) -> None:
        """Test getting files by entity."""

        # Save multiple files for same entity
        for i in range(3):
            metadata = FileMetadata(
                id=uuid4(),
                filename=f"file{i}.txt",
                content_type="text/plain",
                size=100,
                storage_key=f"file{i}.txt",
                storage_backend="local",
                entity_name="Task",
                entity_id="task-123",
                field_name="attachment",
                created_at=datetime.now(UTC),
                url=f"/files/file{i}.txt",
            )
            metadata_store.save(metadata)

        # Save file for different entity
        other_metadata = FileMetadata(
            id=uuid4(),
            filename="other.txt",
            content_type="text/plain",
            size=100,
            storage_key="other.txt",
            storage_backend="local",
            entity_name="Task",
            entity_id="task-456",
            created_at=datetime.now(UTC),
            url="/files/other.txt",
        )
        metadata_store.save(other_metadata)

        # Get by entity
        files = metadata_store.get_by_entity("Task", "task-123")
        assert len(files) == 3

        # Get by entity and field
        files = metadata_store.get_by_entity("Task", "task-123", "attachment")
        assert len(files) == 3

    def test_update_entity_association(self, metadata_store: Any) -> None:
        """Test updating entity association."""

        metadata = FileMetadata(
            id=uuid4(),
            filename="test.txt",
            content_type="text/plain",
            size=100,
            storage_key="test.txt",
            storage_backend="local",
            created_at=datetime.now(UTC),
            url="/files/test.txt",
        )
        metadata_store.save(metadata)

        # Update association
        updated = metadata_store.update_entity_association(
            metadata.id, "Task", "task-123", "attachment"
        )

        assert updated is True

        # Verify
        retrieved = metadata_store.get(metadata.id)
        assert retrieved.entity_name == "Task"
        assert retrieved.entity_id == "task-123"
        assert retrieved.field_name == "attachment"


# =============================================================================
# FileService Tests
# =============================================================================


@pytest.mark.skipif(
    not os.environ.get("DATABASE_URL"),
    reason="DATABASE_URL not set — FileService requires PostgreSQL",
)
class TestFileService:
    """Tests for FileService."""

    @pytest.mark.asyncio
    async def test_upload(self, file_service: Any, sample_file: Any) -> None:
        """Test uploading a file."""
        metadata = await file_service.upload(
            sample_file,
            "test.txt",
            "text/plain",
        )

        assert metadata.filename == "test.txt"
        assert metadata.content_type == "text/plain"
        assert metadata.size > 0

    @pytest.mark.asyncio
    async def test_upload_with_entity(self, file_service: Any, sample_file: Any) -> None:
        """Test uploading with entity association."""
        metadata = await file_service.upload(
            sample_file,
            "test.txt",
            "text/plain",
            entity_name="Task",
            entity_id="task-123",
            field_name="attachment",
        )

        assert metadata.entity_name == "Task"
        assert metadata.entity_id == "task-123"
        assert metadata.field_name == "attachment"

    @pytest.mark.asyncio
    async def test_upload_validation_fails(self, file_service: Any) -> None:
        """Test upload validation failure."""
        # Create oversized file
        file_service.validator.max_size = 10
        large_file = BytesIO(b"x" * 100)

        with pytest.raises(FileValidationError):
            await file_service.upload(large_file, "large.txt")

    @pytest.mark.asyncio
    async def test_download(self, file_service: Any, sample_file: Any) -> None:
        """Test downloading a file."""
        # Upload first
        metadata = await file_service.upload(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # Download
        content, retrieved_metadata = await file_service.download(metadata.id)

        assert content == b"Hello, World! This is a test file."
        assert retrieved_metadata.id == metadata.id

    @pytest.mark.asyncio
    async def test_download_not_found(self, file_service: Any) -> None:
        """Test downloading non-existent file."""
        with pytest.raises(FileNotFoundError):
            await file_service.download(uuid4())

    @pytest.mark.asyncio
    async def test_delete(self, file_service: Any, sample_file: Any) -> None:
        """Test deleting a file."""
        # Upload first
        metadata = await file_service.upload(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # Delete
        deleted = await file_service.delete(metadata.id)

        assert deleted is True
        assert file_service.get_metadata(metadata.id) is None

    def test_get_metadata(self, file_service: Any) -> None:
        """Test getting metadata without download."""
        # Metadata doesn't exist yet
        result = file_service.get_metadata(uuid4())
        assert result is None

    @pytest.mark.asyncio
    async def test_get_entity_files(self, file_service: Any) -> None:
        """Test getting files for an entity."""
        # Upload files for entity
        for i in range(3):
            file_obj = BytesIO(f"Content {i}".encode())
            await file_service.upload(
                file_obj,
                f"file{i}.txt",
                "text/plain",
                entity_name="Task",
                entity_id="task-123",
                field_name="attachment",
            )

        files = file_service.get_entity_files("Task", "task-123")
        assert len(files) == 3

    @pytest.mark.asyncio
    async def test_associate_with_entity(self, file_service: Any, sample_file: Any) -> None:
        """Test associating uploaded file with entity."""
        # Upload without entity
        metadata = await file_service.upload(
            sample_file,
            "test.txt",
            "text/plain",
        )

        # Associate later
        associated = await file_service.associate_with_entity(
            metadata.id, "Task", "task-123", "attachment"
        )

        assert associated is True

        # Verify
        files = file_service.get_entity_files("Task", "task-123", "attachment")
        assert len(files) == 1
        assert files[0].id == metadata.id


# =============================================================================
# Factory Function Tests
# =============================================================================


@pytest.mark.skipif(
    not os.environ.get("DATABASE_URL"),
    reason="DATABASE_URL not set — factory functions require PostgreSQL",
)
class TestFactoryFunctions:
    """Tests for factory functions."""

    def test_create_local_file_service(self, tmp_path: Any) -> None:
        """Test creating local file service."""
        service = create_local_file_service(
            base_path=tmp_path / "uploads",
            database_url=os.environ["DATABASE_URL"],
            base_url="/uploads",
            max_size=5 * 1024 * 1024,
            allowed_types=["image/*", "application/pdf"],
        )

        assert service is not None
        assert service.validator.max_size == 5 * 1024 * 1024
        assert service.validator.allowed_types == ["image/*", "application/pdf"]
