"""
Unit tests for :class:`telestore.TeleStoreClient`.

All HTTP calls are mocked via the ``responses`` library so that no live
network connection is required.
"""
from __future__ import annotations

import io
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import patch

import pytest
import responses as resp_mock

from telestore import (
    TeleStoreClient,
    AuthenticationError,
    DownloadError,
    FileSizeError,
    ServerError,
    UploadError,
    UploadedFile,
)

# ---------------------------------------------------------------------------
# Constants shared across tests
# ---------------------------------------------------------------------------
BASE_URL = "https://telestore.onrender.com"
API_KEY = "sk-test-key-123"
FILE_ID = "abc123"

UPLOAD_RESPONSE = {
    "status": "success",
    "file_id": FILE_ID,
    "view_url": f"{BASE_URL}/files/view/{FILE_ID}/",
    "download_url": f"{BASE_URL}/files/download/{FILE_ID}/",
    "upload_method": "bot",
}

UPLOAD_URL = f"{BASE_URL}/files/upload/"
DOWNLOAD_URL = f"{BASE_URL}/files/download/{FILE_ID}/"
VIEW_URL = f"{BASE_URL}/files/view/{FILE_ID}/"


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def client() -> TeleStoreClient:
    return TeleStoreClient(api_key=API_KEY, base_url=BASE_URL)


@pytest.fixture
def sample_bytes() -> bytes:
    # 10 bytes of content
    return b"hello test"


@pytest.fixture
def sample_file(tmp_path: Path, sample_bytes: bytes) -> Path:
    p = tmp_path / "sample.txt"
    p.write_bytes(sample_bytes)
    return p


# ---------------------------------------------------------------------------
# __init__ / constructor tests
# ---------------------------------------------------------------------------
class TestClientInit:
    def test_empty_api_key_raises(self) -> None:
        with pytest.raises(AuthenticationError):
            TeleStoreClient(api_key="")

    def test_whitespace_api_key_raises(self) -> None:
        with pytest.raises(AuthenticationError):
            TeleStoreClient(api_key="   ")

    def test_trailing_slash_stripped(self) -> None:
        c = TeleStoreClient(api_key=API_KEY, base_url="https://example.com/")
        assert c.base_url == "https://example.com"

    def test_x_api_key_header_set(self) -> None:
        c = TeleStoreClient(api_key=API_KEY)
        assert c._session.headers.get("x-api-key") == API_KEY


# ---------------------------------------------------------------------------
# upload() tests
# ---------------------------------------------------------------------------
class TestUpload:
    @resp_mock.activate
    def test_upload_from_path(self, client: TeleStoreClient, sample_file: Path) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)

        result = client.upload(sample_file)

        assert isinstance(result, UploadedFile)
        assert result.file_id == FILE_ID
        assert result.view_url == VIEW_URL
        assert result.download_url == DOWNLOAD_URL
        assert result.original_filename == "sample.txt"
        assert result.size_bytes == 10

    @resp_mock.activate
    def test_upload_from_bytes(self, client: TeleStoreClient, sample_bytes: bytes) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)

        result = client.upload(sample_bytes, filename="raw.bin")

        assert isinstance(result, UploadedFile)
        assert result.file_id == FILE_ID
        assert result.original_filename == "raw.bin"

    @resp_mock.activate
    def test_upload_from_filelike(self, client: TeleStoreClient, sample_bytes: bytes) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)

        filelike = io.BytesIO(sample_bytes)
        filelike.name = "stream.bin"

        result = client.upload(filelike)

        assert isinstance(result, UploadedFile)
        assert result.file_id == FILE_ID
        assert result.original_filename == "stream.bin"

    def test_upload_file_size_error(self, client: TeleStoreClient, sample_file: Path) -> None:
        # Patch get_file_size to return a value over 2 GiB.
        with patch("telestore.client.get_file_size", return_value=2 * 1024**3 + 1):
            with pytest.raises(FileSizeError):
                client.upload(sample_file)

    @resp_mock.activate
    def test_upload_auth_error_on_401(self, client: TeleStoreClient, sample_bytes: bytes) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json={"error": "Unauthorized"}, status=401)

        with pytest.raises(AuthenticationError):
            client.upload(sample_bytes, filename="f.bin")

    @resp_mock.activate
    def test_upload_server_error_on_500(self, client: TeleStoreClient, sample_bytes: bytes) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json={"error": "Internal error"}, status=500)

        with pytest.raises(ServerError):
            client.upload(sample_bytes, filename="f.bin")

    @resp_mock.activate
    def test_upload_error_on_400(self, client: TeleStoreClient, sample_bytes: bytes) -> None:
        resp_mock.add(
            resp_mock.POST,
            UPLOAD_URL,
            json={"error": "No file was submitted."},
            status=400,
        )
        with pytest.raises(UploadError, match="No file was submitted"):
            client.upload(sample_bytes, filename="f.bin")


# ---------------------------------------------------------------------------
# download() tests
# ---------------------------------------------------------------------------
class TestDownload:
    CONTENT = b"binary file content here"

    @resp_mock.activate
    def test_download_returns_bytes(self, client: TeleStoreClient) -> None:
        resp_mock.add(resp_mock.GET, DOWNLOAD_URL, body=self.CONTENT, status=200)

        result = client.download(FILE_ID)

        assert isinstance(result, bytes)
        assert result == self.CONTENT

    @resp_mock.activate
    def test_download_writes_to_file(self, client: TeleStoreClient, tmp_path: Path) -> None:
        resp_mock.add(resp_mock.GET, DOWNLOAD_URL, body=self.CONTENT, status=200)

        dest = tmp_path / "output.bin"
        result = client.download(FILE_ID, destination=dest)

        assert result == dest
        assert dest.read_bytes() == self.CONTENT

    @resp_mock.activate
    def test_download_error_on_failure(self, client: TeleStoreClient) -> None:
        resp_mock.add(
            resp_mock.GET,
            DOWNLOAD_URL,
            json={"error": "Not found"},
            status=404,
        )
        from telestore.exceptions import FileNotFoundError as TSFNF
        with pytest.raises(TSFNF):
            client.download(FILE_ID)

    @resp_mock.activate
    def test_download_creates_parent_dirs(self, client: TeleStoreClient, tmp_path: Path) -> None:
        resp_mock.add(resp_mock.GET, DOWNLOAD_URL, body=self.CONTENT, status=200)

        dest = tmp_path / "new_dir" / "subdir" / "output.bin"
        client.download(FILE_ID, destination=dest)

        assert dest.exists()
        assert dest.read_bytes() == self.CONTENT


# ---------------------------------------------------------------------------
# get_url() tests
# ---------------------------------------------------------------------------
class TestGetUrl:
    def test_view_url_no_network(self, client: TeleStoreClient) -> None:
        url = client.get_url(FILE_ID, mode="view")
        assert url == f"{BASE_URL}/files/view/{FILE_ID}/"

    def test_download_url_no_network(self, client: TeleStoreClient) -> None:
        url = client.get_url(FILE_ID, mode="download")
        assert url == f"{BASE_URL}/files/download/{FILE_ID}/"

    def test_default_mode_is_view(self, client: TeleStoreClient) -> None:
        assert client.get_url(FILE_ID) == client.get_url(FILE_ID, mode="view")

    def test_invalid_mode_raises_value_error(self, client: TeleStoreClient) -> None:
        with pytest.raises(ValueError):
            client.get_url(FILE_ID, mode="stream")


# ---------------------------------------------------------------------------
# Convenience method round-trip tests (mocked HTTP)
# ---------------------------------------------------------------------------
class TestConvenienceMethods:
    @resp_mock.activate
    def test_upload_json_download_json_roundtrip(self, client: TeleStoreClient) -> None:
        data = {"user": "alice", "score": 42}
        json_bytes = json.dumps(data, ensure_ascii=False).encode("utf-8")

        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)
        resp_mock.add(resp_mock.GET, DOWNLOAD_URL, body=json_bytes, status=200)

        upload_result = client.upload_json(data)
        assert isinstance(upload_result, UploadedFile)

        downloaded = client.download_json(upload_result.file_id)
        assert downloaded == data

    @resp_mock.activate
    def test_upload_text_download_text_roundtrip(self, client: TeleStoreClient) -> None:
        text = "Hello, TeleStore!"
        text_bytes = text.encode("utf-8")

        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)
        resp_mock.add(resp_mock.GET, DOWNLOAD_URL, body=text_bytes, status=200)

        upload_result = client.upload_text(text)
        assert isinstance(upload_result, UploadedFile)

        downloaded = client.download_text(upload_result.file_id)
        assert downloaded == text

    @resp_mock.activate
    def test_upload_image_valid(self, client: TeleStoreClient) -> None:
        img_response = dict(UPLOAD_RESPONSE)
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=img_response, status=200)

        # Provide a real minimal PNG (1×1 pixel) so MIME detection works.
        minimal_png = (
            b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
            b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x02"
            b"\x00\x00\x00\x90wS\xde\x00\x00\x00\x0cIDATx"
            b"\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18\xd8N"
            b"\x00\x00\x00\x00IEND\xaeB`\x82"
        )
        with patch("telestore.client.detect_mime_type", return_value="image/png"):
            result = client.upload_image(minimal_png, filename="pixel.png")

        assert result.mime_type == "image/png"

    @resp_mock.activate
    def test_upload_image_invalid_mime_raises(self, client: TeleStoreClient) -> None:
        resp_mock.add(resp_mock.POST, UPLOAD_URL, json=UPLOAD_RESPONSE, status=200)

        with patch("telestore.client.detect_mime_type", return_value="application/pdf"):
            with pytest.raises(UploadError, match="Expected an image"):
                client.upload_image(b"not an image", filename="doc.pdf")
