"""Tests for the TelegramMediaFile domain — model, schemas, and validators.

Pure unit tests — no database, Kafka, or external dependencies.
"""

from __future__ import annotations

from datetime import datetime, timezone

import pytest

from repo_core.base.validators import ValidationError, ValidatorPipeline
from repo_core.domains.telegram.media.model import TelegramMediaFile
from repo_core.domains.telegram.media.schema import (
    CreateMediaFileRequest,
    MediaFileResponse,
    PatchMediaFileRequest,
)
from repo_core.domains.telegram.media.validator import (
    MediaFilePayloadValidator,
    MediaTypeValidator,
    MessageIdValidator,
    PeerIdValidator,
    StorageKeyValidator,
    build_media_file_pipeline,
)


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


@pytest.fixture
def now() -> datetime:
    return datetime.now(timezone.utc)


@pytest.fixture
def valid_media_kwargs(now: datetime) -> dict:
    """Minimal valid kwargs for a TelegramMediaFile (all required fields)."""
    return {
        "peer_id": 123,
        "message_id": 456,
        "storage_key": "telegram/123/456/photo.jpg",
        "bucket": "dream-media",
        "file_size": 1024000,
        "mime_type": "image/jpeg",
        "media_type": "photo",
        "downloaded_at": now,
    }


# =========================================================================
# Model tests
# =========================================================================


class TestTelegramMediaFileModel:
    def test_from_db_creates_instance(self, valid_media_kwargs: dict, now: datetime):
        media = TelegramMediaFile.from_db(valid_media_kwargs)
        assert media.peer_id == 123
        assert media.message_id == 456
        assert media.storage_key == "telegram/123/456/photo.jpg"
        assert media.bucket == "dream-media"
        assert media.file_size == 1024000
        assert media.mime_type == "image/jpeg"
        assert media.media_type == "photo"
        assert media.downloaded_at == now

    def test_from_db_optional_fields_default_to_none(self):
        """Fields without defaults should be None when missing from input dict."""
        media = TelegramMediaFile.from_db({
            "peer_id": 1,
            "message_id": 2,
            "storage_key": "k",
            "bucket": "b",
            "file_size": 100,
            "mime_type": "text/plain",
            "media_type": "document",
            "downloaded_at": datetime.now(timezone.utc),
        })
        assert media.width is None
        assert media.height is None
        assert media.duration_seconds is None
        assert media.created_at is None
        assert media.updated_at is None

    def test_to_db_returns_full_dict(self, valid_media_kwargs: dict):
        media = TelegramMediaFile(**valid_media_kwargs)
        row = media.to_db()
        assert row["peer_id"] == 123
        assert row["bucket"] == "dream-media"
        assert row["file_size"] == 1024000

    def test_to_db_round_trip(self, valid_media_kwargs: dict):
        media = TelegramMediaFile(**valid_media_kwargs)
        row = media.to_db()
        restored = TelegramMediaFile.from_db(row)
        assert restored.peer_id == media.peer_id
        assert restored.storage_key == media.storage_key
        assert restored.bucket == media.bucket
        assert restored.file_size == media.file_size
        assert restored.downloaded_at == media.downloaded_at

    def test_to_db_partial_only_set_fields(self):
        """model_construct + to_db_partial should only include explicitly set fields."""
        media = TelegramMediaFile.model_construct(
            peer_id=1,
            message_id=2,
            storage_key="k",
            bucket="b",
            file_size=100,
            mime_type="text/plain",
            media_type="document",
            downloaded_at=datetime.now(timezone.utc),
        )
        partial = media.to_db_partial()
        assert "peer_id" in partial
        assert "storage_key" in partial
        assert "width" not in partial     # never set

    def test_model_rejects_missing_required_fields(self):
        """Pydantic validates required fields on construction."""
        with pytest.raises(Exception):  # ValidationError
            TelegramMediaFile(peer_id=1)  # type: ignore[arg-type]

    def test_model_equality(self, valid_media_kwargs: dict):
        a = TelegramMediaFile(**valid_media_kwargs)
        b = TelegramMediaFile(**valid_media_kwargs)
        assert a == b

    def test_model_serialize_to_json(self, valid_media_kwargs: dict):
        media = TelegramMediaFile(**valid_media_kwargs)
        json_str = media.model_dump_json()
        assert "dream-media" in json_str
        assert "file_size" in json_str

    def test_timestamps_are_declared(self):
        """_timestamps is declared as a class-level set on DomainModel subclasses."""
        assert hasattr(TelegramMediaFile, "_timestamps")

    def test_db_include_has_core_fields(self):
        assert hasattr(TelegramMediaFile, "_db_include")

    def test_warehouse_include_has_core_fields(self):
        assert hasattr(TelegramMediaFile, "_warehouse_include")


# =========================================================================
# Schema tests
# =========================================================================


class TestCreateMediaFileRequest:
    def test_valid_creation(self, now: datetime):
        req = CreateMediaFileRequest(
            peer_id=1,
            message_id=2,
            storage_key="k",
            bucket="b",
            file_size=100,
            mime_type="image/png",
            media_type="photo",
            downloaded_at=now,
        )
        assert req.peer_id == 1
        assert req.bucket == "b"
        assert req.width is None        # default

    def test_optional_fields_set(self, now: datetime):
        req = CreateMediaFileRequest(
            peer_id=1, message_id=2,
            storage_key="k", bucket="b",
            file_size=500, mime_type="video/mp4", media_type="video",
            width=1920, height=1080, duration_seconds=45.5,
            downloaded_at=now,
        )
        assert req.width == 1920
        assert req.duration_seconds == 45.5

    def test_rejects_extra_fields(self, now: datetime):
        with pytest.raises(Exception):
            CreateMediaFileRequest(
                peer_id=1, message_id=2,
                storage_key="k", bucket="b",
                file_size=100, mime_type="image/png", media_type="photo",
                downloaded_at=now,
                extra_field="should not be here",  # type: ignore[call-arg]
            )

    def test_rejects_missing_required(self):
        with pytest.raises(Exception):
            CreateMediaFileRequest(peer_id=1)  # type: ignore[arg-type]


class TestMediaFileResponse:
    def test_from_attributes(self, now: datetime):
        class FakeORM:
            def __init__(self):
                self.peer_id = 1
                self.message_id = 2
                self.storage_key = "k"
                self.bucket = "b"
                self.file_size = 100
                self.mime_type = "image/png"
                self.media_type = "photo"
                self.width = None
                self.height = None
                self.duration_seconds = None
                self.downloaded_at = now
                self.created_at = None
                self.updated_at = None

        resp = MediaFileResponse.model_validate(FakeORM(), from_attributes=True)
        assert resp.peer_id == 1
        assert resp.bucket == "b"

    def test_to_dict(self, now: datetime):
        resp = MediaFileResponse(
            peer_id=1, message_id=2,
            storage_key="k", bucket="b",
            file_size=100, mime_type="image/png", media_type="photo",
            downloaded_at=now,
        )
        d = resp.to_dict()
        assert d["bucket"] == "b"
        assert d["file_size"] == 100


class TestPatchMediaFileRequest:
    def test_empty_patch(self):
        patch = PatchMediaFileRequest()
        assert patch.to_patch_dict() == {}

    def test_partial_fields(self):
        patch = PatchMediaFileRequest(bucket="new-bucket", file_size=2000)
        result = patch.to_patch_dict()
        assert result == {"bucket": "new-bucket", "file_size": 2000}

    def test_excludes_none_fields(self):
        """to_patch_dict includes explicitly-set fields even when None."""
        patch = PatchMediaFileRequest(bucket="new-bucket", mime_type=None)
        result = patch.to_patch_dict()
        # mime_type is explicitly set to None → included
        assert "bucket" in result
        assert "mime_type" in result


# =========================================================================
# Validator tests  (all async — validators are coroutines)
# =========================================================================


class TestStorageKeyValidator:
    @pytest.mark.asyncio
    async def test_passes_valid_key(self):
        v = StorageKeyValidator()
        result = await v.validate("telegram/123/456/photo.jpg")
        assert result == "telegram/123/456/photo.jpg"

    @pytest.mark.asyncio
    async def test_strips_whitespace(self):
        v = StorageKeyValidator()
        result = await v.validate("  key  ")
        assert result == "key"

    @pytest.mark.asyncio
    async def test_rejects_empty_string(self):
        v = StorageKeyValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate("")
        assert exc_info.value.field == "storage_key"

    @pytest.mark.asyncio
    async def test_rejects_whitespace_only(self):
        v = StorageKeyValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate("   ")
        assert exc_info.value.field == "storage_key"

    @pytest.mark.asyncio
    async def test_rejects_non_string(self):
        v = StorageKeyValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(123)  # type: ignore[arg-type]
        assert exc_info.value.field == "storage_key"

    @pytest.mark.asyncio
    async def test_rejects_over_max_length(self):
        v = StorageKeyValidator()
        too_long = "x" * (StorageKeyValidator.MAX_LENGTH + 1)
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(too_long)
        assert exc_info.value.field == "storage_key"

    @pytest.mark.asyncio
    async def test_accepts_at_max_length(self):
        v = StorageKeyValidator()
        at_limit = "x" * StorageKeyValidator.MAX_LENGTH
        result = await v.validate(at_limit)
        assert len(result) == StorageKeyValidator.MAX_LENGTH


class TestMediaTypeValidator:
    @pytest.mark.asyncio
    async def test_passes_valid_types(self):
        v = MediaTypeValidator()
        for media_type in ("photo", "video", "document", "gif", "audio", "sticker"):
            result = await v.validate(media_type)
            assert result == media_type

    @pytest.mark.asyncio
    async def test_rejects_invalid_type(self):
        v = MediaTypeValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate("unknown")
        assert exc_info.value.field == "media_type"

    @pytest.mark.asyncio
    async def test_rejects_empty_string(self):
        v = MediaTypeValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate("")
        assert exc_info.value.field == "media_type"


class TestPeerIdValidator:
    @pytest.mark.asyncio
    async def test_passes_positive_integer(self):
        v = PeerIdValidator()
        result = await v.validate(123)
        assert result == 123

    @pytest.mark.asyncio
    async def test_rejects_non_integer(self):
        v = PeerIdValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate("abc")  # type: ignore[arg-type]
        assert exc_info.value.field == "peer_id"

    @pytest.mark.asyncio
    async def test_rejects_zero(self):
        v = PeerIdValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(0)
        assert exc_info.value.field == "peer_id"

    @pytest.mark.asyncio
    async def test_rejects_negative(self):
        v = PeerIdValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(-100)
        assert exc_info.value.field == "peer_id"


class TestMessageIdValidator:
    @pytest.mark.asyncio
    async def test_passes_positive_integer(self):
        v = MessageIdValidator()
        result = await v.validate(456)
        assert result == 456

    @pytest.mark.asyncio
    async def test_rejects_non_integer(self):
        v = MessageIdValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(None)  # type: ignore[arg-type]
        assert exc_info.value.field == "message_id"

    @pytest.mark.asyncio
    async def test_rejects_zero(self):
        v = MessageIdValidator()
        with pytest.raises(ValidationError) as exc_info:
            await v.validate(0)
        assert exc_info.value.field == "message_id"


# =========================================================================
# Pipeline tests
# =========================================================================


class TestBuildMediaFilePipeline:
    """Pipeline takes a dict with storage_key, file_size, media_type keys."""

    @pytest.mark.asyncio
    async def test_returns_pipeline_instance(self):
        pipeline = build_media_file_pipeline()
        assert isinstance(pipeline, ValidatorPipeline)

    @pytest.mark.asyncio
    async def test_accepts_valid_data(self):
        pipeline = build_media_file_pipeline()
        result = await pipeline.validate({
            "storage_key": "telegram/123/456/doc.pdf",
            "media_type": "document",
        })
        assert result["storage_key"] == "telegram/123/456/doc.pdf"
        assert result["media_type"] == "document"

    @pytest.mark.asyncio
    async def test_strips_storage_key_whitespace(self):
        pipeline = build_media_file_pipeline()
        result = await pipeline.validate({
            "storage_key": "  k  ",
            "media_type": "photo",
        })
        assert result["storage_key"] == "k"

    @pytest.mark.asyncio
    async def test_stops_on_first_error(self):
        """Pipeline stops at the first failing validator (storage_key)."""
        pipeline = build_media_file_pipeline()
        with pytest.raises(ValidationError) as exc_info:
            await pipeline.validate({
                "storage_key": "",
                "media_type": "unknown",
            })
        assert exc_info.value.field == "storage_key"

    @pytest.mark.asyncio
    async def test_media_type_validated_second(self):
        """After storage_key passes, media_type is checked."""
        pipeline = build_media_file_pipeline()
        with pytest.raises(ValidationError) as exc_info:
            await pipeline.validate({
                "storage_key": "valid-key",
                "file_size": 100,
                "media_type": "email",
            })
        assert exc_info.value.field == "media_type"

    @pytest.mark.asyncio
    async def test_rejects_non_dict(self):
        pipeline = build_media_file_pipeline()
        with pytest.raises(ValidationError) as exc_info:
            await pipeline.validate("not-a-dict")
        assert exc_info.value.field == "payload"

    @pytest.mark.asyncio
    async def test_missing_key_uses_default(self):
        """Missing media_type defaults to empty string — fails MediaTypeValidator."""
        pipeline = build_media_file_pipeline()
        with pytest.raises(ValidationError) as exc_info:
            await pipeline.validate({"storage_key": "k"})
        assert exc_info.value.field == "media_type"


class TestMediaFilePayloadValidator:
    @pytest.mark.asyncio
    async def test_passes_extra_keys_unchanged(self):
        """Extra keys in the dict are preserved."""
        v = MediaFilePayloadValidator()
        result = await v.validate({
            "storage_key": "k",
            "media_type": "photo",
            "extra_field": "keep-me",
        })
        assert result["extra_field"] == "keep-me"
