"""Tests for X (Twitter) provider."""

import base64
from datetime import datetime, timedelta
from typing import Optional
from unittest.mock import AsyncMock, MagicMock, patch

import httpx
import pytest

from nospoon_integrations.core.errors import ProviderAPIError, TokenNotFoundError
from nospoon_integrations.core.types import OAuthCallbackParams, ProviderConfig, TokenData
from nospoon_integrations.providers.x import (
    CreatePostParams,
    UploadMediaParams,
    XProvider,
)


class MockTokenStorage:
    """Simple in-memory token storage for tests."""

    def __init__(self) -> None:
        self._tokens: dict = {}

    async def get_tokens(self, user_id: str, provider: str) -> Optional[TokenData]:
        return self._tokens.get(f"{user_id}:{provider}")

    async def save_tokens(self, user_id: str, provider: str, tokens: TokenData) -> None:
        self._tokens[f"{user_id}:{provider}"] = tokens

    async def delete_tokens(self, user_id: str, provider: str) -> None:
        self._tokens.pop(f"{user_id}:{provider}", None)

    async def has_tokens(self, user_id: str, provider: str) -> bool:
        return f"{user_id}:{provider}" in self._tokens


@pytest.fixture
def storage() -> MockTokenStorage:
    return MockTokenStorage()


@pytest.fixture
def config() -> ProviderConfig:
    return ProviderConfig(
        client_id="test-client-id",
        client_secret="test-client-secret",
    )


@pytest.fixture
def provider(config: ProviderConfig, storage: MockTokenStorage) -> XProvider:
    return XProvider(config, storage)


def mock_response(
    status_code: int = 200, json_data: Optional[dict] = None, text: str = ""
) -> httpx.Response:
    """Create a mock httpx Response."""
    response = MagicMock(spec=httpx.Response)
    response.status_code = status_code
    response.is_success = 200 <= status_code < 300
    response.json.return_value = json_data or {}
    response.text = text
    return response


class TestConstructor:
    def test_default_scopes(self, provider: XProvider) -> None:
        url = provider.get_auth_url("https://example.com/callback")
        assert "tweet.read" in url
        assert "tweet.write" in url
        assert "users.read" in url
        assert "offline.access" in url

    def test_custom_scopes(self, storage: MockTokenStorage) -> None:
        config = ProviderConfig(
            client_id="test",
            client_secret="test",
            scopes=["tweet.read", "users.read"],
        )
        p = XProvider(config, storage)
        url = p.get_auth_url("https://example.com/callback")
        assert "tweet.read" in url
        assert "users.read" in url
        assert "tweet.write" not in url


class TestAuthUrl:
    def test_correct_url(self, provider: XProvider) -> None:
        url = provider.get_auth_url("https://example.com/callback")
        assert "x.com/i/oauth2/authorize" in url
        assert "client_id=test-client-id" in url
        assert "response_type=code" in url


class TestTokenExchange:
    @pytest.mark.asyncio
    async def test_basic_auth_header(self, provider: XProvider) -> None:
        resp = mock_response(
            json_data={
                "access_token": "access",
                "refresh_token": "refresh",
                "expires_in": 7200,
            }
        )
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            await provider.exchange_code("code", "https://example.com/callback")
            call_kwargs = provider._client.post.call_args
            headers = call_kwargs.kwargs.get("headers") or call_kwargs[1].get("headers", {})
            expected = base64.b64encode(b"test-client-id:test-client-secret").decode()
            assert headers.get("Authorization") == f"Basic {expected}"

    @pytest.mark.asyncio
    async def test_exchange_code(self, provider: XProvider) -> None:
        resp = mock_response(
            json_data={
                "access_token": "x-access",
                "refresh_token": "x-refresh",
                "expires_in": 7200,
                "scope": "tweet.read tweet.write",
            }
        )
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            result = await provider.exchange_code("code", "https://example.com/callback")
            assert result.access_token == "x-access"
            assert result.refresh_token == "x-refresh"

    @pytest.mark.asyncio
    async def test_exchange_failure(self, provider: XProvider) -> None:
        resp = mock_response(status_code=400, text="Invalid code")
        with (
            patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp),
            pytest.raises(Exception, match="Token exchange failed"),
        ):
            await provider.exchange_code("invalid", "https://example.com/callback")


class TestRefreshToken:
    @pytest.mark.asyncio
    async def test_rotating_refresh(self, provider: XProvider) -> None:
        resp = mock_response(
            json_data={
                "access_token": "new-access",
                "refresh_token": "new-refresh",
                "expires_in": 7200,
            }
        )
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            result = await provider.refresh_token("old-refresh")
            assert result.access_token == "new-access"
            assert result.refresh_token == "new-refresh"
            assert result.expires_in == 7200


class TestGetUserInfo:
    @pytest.mark.asyncio
    async def test_success(self, provider: XProvider, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

        resp = mock_response(
            json_data={
                "data": {
                    "id": "12345",
                    "name": "Test User",
                    "username": "testuser",
                    "profile_image_url": "https://pbs.twimg.com/photo.jpg",
                    "description": "A test account",
                }
            }
        )
        with patch.object(provider._client, "get", new_callable=AsyncMock, return_value=resp):
            info = await provider.get_user_info("user-1")
            assert info.id == "12345"
            assert info.name == "Test User"
            assert info.username == "testuser"
            assert info.profile_image_url == "https://pbs.twimg.com/photo.jpg"
            assert info.description == "A test account"

    @pytest.mark.asyncio
    async def test_failure(self, provider: XProvider, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

        resp = mock_response(status_code=401, text="Unauthorized")
        with (
            patch.object(provider._client, "get", new_callable=AsyncMock, return_value=resp),
            pytest.raises(ProviderAPIError),
        ):
            await provider.get_user_info("user-1")

    @pytest.mark.asyncio
    async def test_no_token(self, provider: XProvider) -> None:
        with pytest.raises(TokenNotFoundError):
            await provider.get_user_info("user-1")


class TestCreatePost:
    @pytest.fixture(autouse=True)
    async def setup_token(self, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

    @pytest.mark.asyncio
    async def test_simple_post(self, provider: XProvider) -> None:
        resp = mock_response(json_data={"data": {"id": "tweet-123", "text": "Hello world!"}})
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            result = await provider.create_post("user-1", CreatePostParams(text="Hello world!"))
            assert result.id == "tweet-123"
            assert result.text == "Hello world!"

    @pytest.mark.asyncio
    async def test_reply(self, provider: XProvider) -> None:
        resp = mock_response(json_data={"data": {"id": "reply-123", "text": "A reply"}})
        with patch.object(
            provider._client, "post", new_callable=AsyncMock, return_value=resp
        ) as mock_post:
            await provider.create_post(
                "user-1",
                CreatePostParams(text="A reply", reply_to_tweet_id="original-id"),
            )
            call_kwargs = mock_post.call_args
            body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json", {})
            assert body["reply"]["in_reply_to_tweet_id"] == "original-id"

    @pytest.mark.asyncio
    async def test_quote_tweet(self, provider: XProvider) -> None:
        resp = mock_response(json_data={"data": {"id": "qt-123", "text": "My quote"}})
        with patch.object(
            provider._client, "post", new_callable=AsyncMock, return_value=resp
        ) as mock_post:
            await provider.create_post(
                "user-1",
                CreatePostParams(text="My quote", quote_tweet_id="quoted-id"),
            )
            call_kwargs = mock_post.call_args
            body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json", {})
            assert body["quote_tweet_id"] == "quoted-id"

    @pytest.mark.asyncio
    async def test_with_media(self, provider: XProvider) -> None:
        resp = mock_response(json_data={"data": {"id": "m-123", "text": "With media"}})
        with patch.object(
            provider._client, "post", new_callable=AsyncMock, return_value=resp
        ) as mock_post:
            await provider.create_post(
                "user-1",
                CreatePostParams(text="With media", media_ids=["m1", "m2"]),
            )
            call_kwargs = mock_post.call_args
            body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json", {})
            assert body["media"]["media_ids"] == ["m1", "m2"]

    @pytest.mark.asyncio
    async def test_with_poll(self, provider: XProvider) -> None:
        resp = mock_response(json_data={"data": {"id": "p-123", "text": "Vote!"}})
        with patch.object(
            provider._client, "post", new_callable=AsyncMock, return_value=resp
        ) as mock_post:
            await provider.create_post(
                "user-1",
                CreatePostParams(
                    text="Vote!",
                    poll_options=["Yes", "No"],
                    poll_duration_minutes=60,
                ),
            )
            call_kwargs = mock_post.call_args
            body = call_kwargs.kwargs.get("json") or call_kwargs[1].get("json", {})
            assert body["poll"]["options"] == ["Yes", "No"]
            assert body["poll"]["duration_minutes"] == 60

    @pytest.mark.asyncio
    async def test_failure(self, provider: XProvider) -> None:
        resp = mock_response(status_code=403, text="Forbidden")
        with (
            patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp),
            pytest.raises(ProviderAPIError),
        ):
            await provider.create_post("user-1", CreatePostParams(text="test"))


class TestUploadMedia:
    @pytest.fixture(autouse=True)
    async def setup_token(self, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

    @pytest.mark.asyncio
    async def test_upload_success(self, provider: XProvider) -> None:
        resp = mock_response(
            json_data={"media_id_string": "media-789", "expires_after_secs": 86400}
        )
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            result = await provider.upload_media(
                "user-1",
                UploadMediaParams(media=b"\x89PNG", mime_type="image/png"),
            )
            assert result.media_id == "media-789"
            assert result.expires_after_secs == 86400

    @pytest.mark.asyncio
    async def test_upload_with_alt_text(self, provider: XProvider) -> None:
        upload_resp = mock_response(json_data={"media_id_string": "media-789"})
        alt_resp = mock_response()
        with patch.object(
            provider._client,
            "post",
            new_callable=AsyncMock,
            side_effect=[upload_resp, alt_resp],
        ):
            await provider.upload_media(
                "user-1",
                UploadMediaParams(media=b"\x89PNG", mime_type="image/png", alt_text="A test image"),
            )
            assert provider._client.post.call_count == 2

    @pytest.mark.asyncio
    async def test_upload_failure(self, provider: XProvider) -> None:
        resp = mock_response(status_code=413, text="File too large")
        with (
            patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp),
            pytest.raises(ProviderAPIError),
        ):
            await provider.upload_media(
                "user-1",
                UploadMediaParams(media=b"\x89PNG", mime_type="image/png"),
            )


class TestDeletePost:
    @pytest.mark.asyncio
    async def test_success(self, provider: XProvider, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

        resp = mock_response(json_data={"data": {"deleted": True}})
        with patch.object(provider._client, "delete", new_callable=AsyncMock, return_value=resp):
            await provider.delete_post("user-1", "tweet-id")

    @pytest.mark.asyncio
    async def test_failure(self, provider: XProvider, storage: MockTokenStorage) -> None:
        await storage.save_tokens(
            "user-1",
            "x",
            TokenData(
                access_token="valid-token",
                expires_at=datetime.now() + timedelta(hours=1),
            ),
        )

        resp = mock_response(status_code=404, text="Not found")
        with (
            patch.object(provider._client, "delete", new_callable=AsyncMock, return_value=resp),
            pytest.raises(ProviderAPIError),
        ):
            await provider.delete_post("user-1", "nonexistent")


class TestHandleCallback:
    @pytest.mark.asyncio
    async def test_stores_tokens(self, provider: XProvider, storage: MockTokenStorage) -> None:
        resp = mock_response(
            json_data={
                "access_token": "new-access",
                "refresh_token": "new-refresh",
                "expires_in": 7200,
            }
        )
        with patch.object(provider._client, "post", new_callable=AsyncMock, return_value=resp):
            await provider.handle_callback(
                "user-1",
                OAuthCallbackParams(code="auth-code", redirect_uri="https://example.com/callback"),
            )
            stored = await storage.get_tokens("user-1", "x")
            assert stored is not None
            assert stored.access_token == "new-access"
            assert stored.refresh_token == "new-refresh"
