"""Unit tests for chained fundamentals provider fallback behavior."""

from __future__ import annotations

from datetime import datetime, timezone

import pytest

from app.exceptions import ProviderError, RateLimitError
from app.providers.fundamentals_chain import ChainedFundamentalProvider
from app.providers.protocol import FundamentalData


class _FailProvider:
    def __init__(self, message: str) -> None:
        self._message = message

    async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
        raise ProviderError(f"{self._message} ({ticker})")


class _RateLimitedProvider:
    async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
        raise RateLimitError(f"rate limit hit for {ticker}")


class _DataProvider:
    def __init__(self, data: FundamentalData) -> None:
        self._data = data

    async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
        return FundamentalData(
            ticker=ticker,
            pe_ratio=self._data.pe_ratio,
            revenue_growth=self._data.revenue_growth,
            earnings_surprise=self._data.earnings_surprise,
            market_cap=self._data.market_cap,
            fetched_at=self._data.fetched_at,
            unavailable_fields=self._data.unavailable_fields,
        )


@pytest.mark.asyncio
async def test_chained_provider_uses_fallback_provider_on_primary_failure():
    fallback_data = FundamentalData(
        ticker="AAPL",
        pe_ratio=25.0,
        revenue_growth=None,
        earnings_surprise=None,
        market_cap=1_000_000.0,
        fetched_at=datetime.now(timezone.utc),
        unavailable_fields={},
    )

    provider = ChainedFundamentalProvider([
        ("primary", _FailProvider("primary down")),
        ("fallback", _DataProvider(fallback_data)),
    ])

    result = await provider.fetch_fundamentals("AAPL")

    assert result.pe_ratio == 25.0
    assert result.market_cap == 1_000_000.0
    assert result.unavailable_fields.get("source_pe_ratio") == "fallback"


@pytest.mark.asyncio
async def test_chained_provider_merges_fields_across_providers():
    """Primary supplies only market cap; fallback fills P/E and earnings."""
    primary_data = FundamentalData(
        ticker="AAPL", pe_ratio=None, revenue_growth=None, earnings_surprise=None,
        market_cap=2_000_000.0, fetched_at=datetime.now(timezone.utc), unavailable_fields={},
    )
    fallback_data = FundamentalData(
        ticker="AAPL", pe_ratio=18.0, revenue_growth=12.0, earnings_surprise=4.0,
        market_cap=999.0, fetched_at=datetime.now(timezone.utc), unavailable_fields={},
    )

    provider = ChainedFundamentalProvider([
        ("fmp", _DataProvider(primary_data)),
        ("finnhub", _DataProvider(fallback_data)),
    ])

    result = await provider.fetch_fundamentals("AAPL")

    # market cap from primary (first to supply it), the rest from fallback
    assert result.market_cap == 2_000_000.0
    assert result.pe_ratio == 18.0
    assert result.revenue_growth == 12.0
    assert result.earnings_surprise == 4.0
    assert result.unavailable_fields.get("source_market_cap") == "fmp"
    assert result.unavailable_fields.get("source_pe_ratio") == "finnhub"


@pytest.mark.asyncio
async def test_chained_provider_raises_when_all_providers_fail():
    provider = ChainedFundamentalProvider([
        ("p1", _FailProvider("p1 failed")),
        ("p2", _FailProvider("p2 failed")),
    ])

    with pytest.raises(ProviderError) as exc:
        await provider.fetch_fundamentals("MSFT")

    assert "All fundamentals providers failed" in str(exc.value)


@pytest.mark.asyncio
async def test_rate_limited_fallback_raises_when_incomplete():
    """FMP gives market cap; the fallback is rate-limited → chain signals it so
    the collector can back off instead of storing a degraded record."""
    primary_data = FundamentalData(
        ticker="AAPL", pe_ratio=None, revenue_growth=None, earnings_surprise=None,
        market_cap=2_000_000.0, fetched_at=datetime.now(timezone.utc), unavailable_fields={},
    )
    provider = ChainedFundamentalProvider([
        ("fmp", _DataProvider(primary_data)),
        ("finnhub", _RateLimitedProvider()),
    ])

    with pytest.raises(RateLimitError):
        await provider.fetch_fundamentals("AAPL")


@pytest.mark.asyncio
async def test_rate_limited_fallback_allows_partial():
    """With allow_partial=True the chain returns the market cap it did get."""
    primary_data = FundamentalData(
        ticker="AAPL", pe_ratio=None, revenue_growth=None, earnings_surprise=None,
        market_cap=2_000_000.0, fetched_at=datetime.now(timezone.utc), unavailable_fields={},
    )
    provider = ChainedFundamentalProvider([
        ("fmp", _DataProvider(primary_data)),
        ("finnhub", _RateLimitedProvider()),
    ])

    result = await provider.fetch_fundamentals("AAPL", allow_partial=True)
    assert result.market_cap == 2_000_000.0
    assert result.pe_ratio is None


@pytest.mark.asyncio
async def test_rate_limited_but_complete_does_not_raise():
    """If every field is filled, a rate limit on a later (unused) provider is moot."""
    full = FundamentalData(
        ticker="AAPL", pe_ratio=20.0, revenue_growth=10.0, earnings_surprise=2.0,
        market_cap=5.0, fetched_at=datetime.now(timezone.utc), unavailable_fields={},
    )
    provider = ChainedFundamentalProvider([
        ("fmp", _DataProvider(full)),
        ("finnhub", _RateLimitedProvider()),
    ])

    result = await provider.fetch_fundamentals("AAPL")
    assert result.pe_ratio == 20.0


@pytest.mark.asyncio
async def test_chain_merges_next_earnings_date():
    """Earnings date is taken from the first provider that supplies it."""
    from datetime import date as _date

    primary = FundamentalData(
        ticker="AAPL", pe_ratio=None, revenue_growth=None, earnings_surprise=None,
        market_cap=100.0, fetched_at=datetime.now(timezone.utc),
    )

    class _EarningsProvider:
        async def fetch_fundamentals(self, ticker: str) -> FundamentalData:
            return FundamentalData(
                ticker=ticker, pe_ratio=10.0, revenue_growth=5.0, earnings_surprise=1.0,
                market_cap=None, fetched_at=datetime.now(timezone.utc),
                next_earnings_date=_date(2026, 7, 1),
            )

    provider = ChainedFundamentalProvider([
        ("fmp", _DataProvider(primary)),
        ("finnhub", _EarningsProvider()),
    ])
    result = await provider.fetch_fundamentals("AAPL")
    assert result.next_earnings_date == _date(2026, 7, 1)
