"""
Unit tests for :mod:`telestore.utils`.
"""
from __future__ import annotations

import io
import sys
from pathlib import Path
from unittest.mock import patch

import pytest

from telestore.utils import detect_mime_type, get_file_size, normalize_source


# ---------------------------------------------------------------------------
# get_file_size
# ---------------------------------------------------------------------------
class TestGetFileSize:
    def test_bytes(self) -> None:
        assert get_file_size(b"hello") == 5

    def test_bytearray(self) -> None:
        assert get_file_size(bytearray(b"hello")) == 5

    def test_path(self, tmp_path: Path) -> None:
        p = tmp_path / "file.bin"
        p.write_bytes(b"a" * 20)
        assert get_file_size(p) == 20

    def test_str_path(self, tmp_path: Path) -> None:
        p = tmp_path / "file.bin"
        p.write_bytes(b"b" * 7)
        assert get_file_size(str(p)) == 7

    def test_filelike_does_not_consume(self) -> None:
        data = b"12345"
        buf = io.BytesIO(data)
        buf.seek(2)  # pre-position the cursor

        size = get_file_size(buf)

        assert size == len(data)
        # cursor must be restored to original position
        assert buf.tell() == 2

    def test_filelike_at_start(self) -> None:
        data = b"x" * 100
        buf = io.BytesIO(data)
        assert get_file_size(buf) == 100
        assert buf.tell() == 0  # cursor restored

    def test_non_seekable_stream_raises_type_error(self) -> None:
        """Non-seekable streams (e.g., HTTP responses, pipes) should raise TypeError."""
        class NonSeekableStream:
            def read(self) -> bytes:
                return b"data"
        
        stream = NonSeekableStream()
        with pytest.raises(TypeError, match="must support .seek\\(\\) and .tell\\(\\)"):
            get_file_size(stream)

    def test_stream_that_claims_to_be_seekable_but_fails(self) -> None:
        """Streams with seek/tell methods that raise OSError should be caught."""
        class BrokenSeekStream:
            def tell(self) -> int:
                raise OSError("Stream doesn't support tell()")
            
            def seek(self, *args) -> None:
                raise OSError("Stream doesn't support seek()")
        
        stream = BrokenSeekStream()
        with pytest.raises(TypeError, match="claims to support seek/tell"):
            get_file_size(stream)


# ---------------------------------------------------------------------------
# normalize_source
# ---------------------------------------------------------------------------
class TestNormalizeSource:
    def test_bytes_gives_upload_filename(self) -> None:
        content, name = normalize_source(b"hello")
        assert content == b"hello"
        assert name == "upload"

    def test_bytearray(self) -> None:
        content, name = normalize_source(bytearray(b"hi"))
        assert content == b"hi"
        assert name == "upload"

    def test_path_gives_real_filename(self, tmp_path: Path) -> None:
        p = tmp_path / "photo.jpg"
        p.write_bytes(b"\xff\xd8\xff")
        content, name = normalize_source(p)
        assert content == b"\xff\xd8\xff"
        assert name == "photo.jpg"

    def test_str_path(self, tmp_path: Path) -> None:
        p = tmp_path / "data.csv"
        p.write_bytes(b"a,b,c")
        content, name = normalize_source(str(p))
        assert name == "data.csv"
        assert content == b"a,b,c"

    def test_missing_path_raises_value_error(self, tmp_path: Path) -> None:
        with pytest.raises(ValueError, match="does not exist"):
            normalize_source(tmp_path / "nonexistent.bin")

    def test_filelike_with_name(self) -> None:
        buf = io.BytesIO(b"data")
        buf.name = "/some/path/report.pdf"
        content, name = normalize_source(buf)
        assert content == b"data"
        assert name == "report.pdf"

    def test_filelike_without_name(self) -> None:
        buf = io.BytesIO(b"data")
        # BytesIO has no .name attribute — our code should fall back
        if hasattr(buf, "name"):
            del buf.name  # type: ignore[misc]
        content, name = normalize_source(buf)
        assert content == b"data"
        assert name == "upload"

    def test_unsupported_type_raises_type_error(self) -> None:
        with pytest.raises(TypeError):
            normalize_source(12345)  # type: ignore[arg-type]

    def test_filelike_non_seekable_raises_type_error(self) -> None:
        """Non-seekable streams should raise TypeError with clear message."""
        class NonSeekableStream:
            def read(self) -> bytes:
                return b"data"
        
        stream = NonSeekableStream()
        with pytest.raises(TypeError, match="requires a seekable file-like object"):
            normalize_source(stream)

    def test_filelike_stream_not_at_position_zero(self) -> None:
        """Stream at non-zero position should read from beginning (not partial)."""
        data = b"0123456789"
        buf = io.BytesIO(data)
        buf.seek(5)  # Position at byte 5
        
        content, name = normalize_source(buf)
        
        # Must read full content from position 0, not from position 5
        assert content == b"0123456789"
        assert len(content) == 10
        # Cursor must be restored to original position (5)
        assert buf.tell() == 5

    def test_filelike_cursor_restored_even_if_read_fails(self) -> None:
        """Cursor must be restored even if read() raises an exception."""
        class FailingStream:
            def __init__(self):
                self.pos = 0
            
            def tell(self) -> int:
                return self.pos
            
            def seek(self, pos: int, whence: int = 0) -> None:
                self.pos = pos
            
            def read(self) -> bytes:
                raise ValueError("Simulated read failure")
        
        stream = FailingStream()
        stream.seek(3)  # Start at position 3
        
        with pytest.raises(ValueError, match="Simulated read failure"):
            normalize_source(stream)
        
        # Cursor must still be restored to position 3
        assert stream.tell() == 3


# ---------------------------------------------------------------------------
# detect_mime_type
# ---------------------------------------------------------------------------
class TestDetectMimeType:
    def test_bytes_fallback_returns_octet_stream(self) -> None:
        # Force magic to be unavailable so we exercise the fallback branch.
        with patch("telestore.utils._MAGIC_AVAILABLE", False):
            mime = detect_mime_type(b"\x00\x01\x02")
        assert mime == "application/octet-stream"

    def test_path_fallback_uses_extension(self, tmp_path: Path) -> None:
        p = tmp_path / "data.json"
        p.write_bytes(b"{}")
        with patch("telestore.utils._MAGIC_AVAILABLE", False):
            mime = detect_mime_type(p)
        assert mime == "application/json"

    def test_path_fallback_unknown_extension(self, tmp_path: Path) -> None:
        p = tmp_path / "data.unknownextension"
        p.write_bytes(b"garbage")
        with patch("telestore.utils._MAGIC_AVAILABLE", False):
            mime = detect_mime_type(p)
        assert mime == "application/octet-stream"

    def test_accepts_str_path(self, tmp_path: Path) -> None:
        p = tmp_path / "image.png"
        p.write_bytes(b"\x89PNG")
        with patch("telestore.utils._MAGIC_AVAILABLE", False):
            mime = detect_mime_type(str(p))
        assert mime == "image/png"
