"""Tests for data providers with HTTP mocking."""

import pytest
import respx
from httpx import Response

from fomo_guard.providers.base import (
    Provider,
    ProviderError,
    RateLimitError,
    CoinNotFoundError,
    ResponseCache,
)
from fomo_guard.providers.coingecko import (
    CoinGeckoProvider,
    validate_symbol,
    SYMBOL_TO_ID,
)


class TestValidateSymbol:
    """Tests for symbol validation."""

    def test_valid_symbols(self):
        assert validate_symbol("BTC") == "BTC"
        assert validate_symbol("btc") == "BTC"
        assert validate_symbol("  eth  ") == "ETH"
        assert validate_symbol("DOGE") == "DOGE"

    def test_invalid_symbols(self):
        with pytest.raises(ValueError, match="non-empty"):
            validate_symbol("")

        with pytest.raises(ValueError, match="non-empty"):
            validate_symbol(None)

        with pytest.raises(ValueError, match="Invalid symbol"):
            validate_symbol("BTC-USD")

        with pytest.raises(ValueError, match="Invalid symbol"):
            validate_symbol("BTC/USD")

        with pytest.raises(ValueError, match="Invalid symbol"):
            validate_symbol("A" * 15)  # Too long


class TestCoinGeckoProvider:
    """Tests for CoinGecko provider."""

    @pytest.fixture
    def provider(self):
        return CoinGeckoProvider()

    @pytest.fixture
    def provider_with_key(self):
        return CoinGeckoProvider(api_key="test-api-key")

    def test_base_url_free_tier(self, provider):
        assert provider.base_url == "https://api.coingecko.com/api/v3"

    def test_base_url_pro_tier(self, provider_with_key):
        assert provider_with_key.base_url == "https://pro-api.coingecko.com/api/v3"

    def test_symbol_mapping(self):
        assert SYMBOL_TO_ID["BTC"] == "bitcoin"
        assert SYMBOL_TO_ID["ETH"] == "ethereum"
        assert "SOL" in SYMBOL_TO_ID

    @respx.mock
    @pytest.mark.asyncio
    async def test_fetch_data_success(self, provider):
        """Test successful data fetch."""
        respx.get("https://api.coingecko.com/api/v3/coins/markets").mock(
            return_value=Response(
                200,
                json=[
                    {
                        "id": "bitcoin",
                        "symbol": "btc",
                        "current_price": 50000,
                        "market_cap": 1000000000000,
                        "ath": 69000,
                        "ath_change_percentage": -27.5,
                        "price_change_percentage_24h": 2.5,
                        "sparkline_in_7d": {"price": [48000, 49000, 50000]},
                    }
                ],
            )
        )

        data = await provider.fetch_data("BTC")

        assert data.symbol == "BTC"
        assert data.current_price == 50000
        assert data.ath == 69000
        assert data.sparkline_7d == [48000, 49000, 50000]

    @respx.mock
    @pytest.mark.asyncio
    async def test_fetch_data_not_found(self, provider):
        """Test handling of non-existent coin."""
        respx.get("https://api.coingecko.com/api/v3/coins/markets").mock(
            return_value=Response(200, json=[])
        )
        respx.get("https://api.coingecko.com/api/v3/search").mock(
            return_value=Response(200, json={"coins": []})
        )

        with pytest.raises(CoinNotFoundError):
            await provider.fetch_data("NOTACOIN")

    @respx.mock
    @pytest.mark.asyncio
    async def test_fetch_data_rate_limited(self, provider):
        """Test rate limit handling."""
        respx.get("https://api.coingecko.com/api/v3/coins/markets").mock(
            return_value=Response(429, headers={"Retry-After": "60"})
        )

        with pytest.raises(RateLimitError) as exc_info:
            await provider.fetch_data("BTC")

        assert exc_info.value.retry_after == 60

    @respx.mock
    @pytest.mark.asyncio
    async def test_fetch_data_with_retry(self, provider):
        """Test retry on transient failure."""
        # First call fails, second succeeds
        route = respx.get("https://api.coingecko.com/api/v3/coins/markets")
        route.side_effect = [
            Response(500),
            Response(
                200,
                json=[{"id": "bitcoin", "symbol": "btc", "current_price": 50000}],
            ),
        ]

        data = await provider.fetch_data("BTC")
        assert data.current_price == 50000

    @respx.mock
    @pytest.mark.asyncio
    async def test_search_coin(self, provider):
        """Test coin search."""
        respx.get("https://api.coingecko.com/api/v3/search").mock(
            return_value=Response(
                200,
                json={
                    "coins": [
                        {"id": "bitcoin", "symbol": "btc", "name": "Bitcoin"},
                        {"id": "bitcoin-cash", "symbol": "bch", "name": "Bitcoin Cash"},
                    ]
                },
            )
        )

        results = await provider.search_coin("bitcoin")

        assert len(results) == 2
        assert results[0]["id"] == "bitcoin"


class TestResponseCache:
    """Tests for response caching."""

    @pytest.fixture
    def cache(self, tmp_path, monkeypatch):
        from fomo_guard.config import Config

        monkeypatch.setattr(
            Config, "get_data_dir", classmethod(lambda cls: tmp_path)
        )
        return ResponseCache(ttl_minutes=5)

    def test_cache_miss(self, cache):
        """Test cache miss returns None."""
        result = cache.get("provider", "/endpoint", {"param": "value"})
        assert result is None

    def test_cache_hit(self, cache):
        """Test cache stores and retrieves data."""
        data = {"key": "value", "nested": {"a": 1}}

        cache.set("provider", "/endpoint", {"param": "value"}, data)
        result = cache.get("provider", "/endpoint", {"param": "value"})

        assert result == data

    def test_cache_different_params(self, cache):
        """Test cache differentiates by params."""
        cache.set("provider", "/endpoint", {"a": 1}, {"data": "first"})
        cache.set("provider", "/endpoint", {"a": 2}, {"data": "second"})

        assert cache.get("provider", "/endpoint", {"a": 1})["data"] == "first"
        assert cache.get("provider", "/endpoint", {"a": 2})["data"] == "second"

    def test_cache_clear(self, cache):
        """Test cache clear."""
        cache.set("provider", "/endpoint", None, {"data": "test"})
        cache.clear()

        assert cache.get("provider", "/endpoint", None) is None
