"""Tests for stock_client.StockClient."""
import asyncio
import logging
import pytest
import httpx

from ai_trade.stock_client import StockClient


BASE_URL = "https://test.stock.local"
API_KEY = "test-secret"


async def test_get_portfolio_normal_returns_dict_with_pnl_pct(mock_stock_api):
    """정상 200 응답 + cache 저장."""
    mock_stock_api.get("/api/webai/portfolio").mock(
        return_value=httpx.Response(
            200,
            json={
                "holdings": [{"ticker": "005930", "pnl_pct": 0.047}],
                "cash": [],
                "summary": {},
            },
        )
    )
    client = StockClient(BASE_URL, API_KEY)
    try:
        result = await client.get_portfolio()
        assert result["holdings"][0]["pnl_pct"] == 0.047
        # Cache populated
        assert len(client._cache) >= 1
    finally:
        await client.close()


async def test_get_portfolio_uses_cache_within_ttl(mock_stock_api):
    """180s TTL 내 두번째 호출 = mock 콜 1회."""
    route = mock_stock_api.get("/api/webai/portfolio").mock(
        return_value=httpx.Response(
            200, json={"holdings": [], "cash": [], "summary": {}}
        )
    )
    client = StockClient(BASE_URL, API_KEY)
    try:
        await client.get_portfolio()
        await client.get_portfolio()  # second call within TTL
        assert route.call_count == 1
    finally:
        await client.close()


async def test_get_portfolio_refetches_after_ttl_expiry(mock_stock_api, monkeypatch):
    """TTL 만료 후 재호출 = mock 콜 2회. time.monotonic 모킹."""
    route = mock_stock_api.get("/api/webai/portfolio").mock(
        return_value=httpx.Response(
            200, json={"holdings": [], "cash": [], "summary": {}}
        )
    )
    # Fake clock: starts at 0, jumps past portfolio TTL (180s) between calls
    fake_time = [0.0]
    monkeypatch.setattr(
        "ai_trade.stock_client.time.monotonic", lambda: fake_time[0]
    )

    client = StockClient(BASE_URL, API_KEY)
    try:
        await client.get_portfolio()
        fake_time[0] = 181.0  # 180s TTL 만료
        await client.get_portfolio()
        assert route.call_count == 2
    finally:
        await client.close()


async def test_get_portfolio_retries_3_times_on_timeout(mock_stock_api, monkeypatch):
    """timeout 2번 + 200 1번 → 최종 성공. exponential sleep 호출 검증."""
    sleep_calls = []

    async def fake_sleep(s):
        sleep_calls.append(s)

    monkeypatch.setattr("asyncio.sleep", fake_sleep)

    mock_stock_api.get("/api/webai/portfolio").mock(
        side_effect=[
            httpx.TimeoutException("timeout 1"),
            httpx.TimeoutException("timeout 2"),
            httpx.Response(
                200, json={"holdings": [], "cash": [], "summary": {}}
            ),
        ]
    )
    client = StockClient(BASE_URL, API_KEY)
    try:
        result = await client.get_portfolio()
        assert result["holdings"] == []
        assert sleep_calls == [1, 2]  # exponential 1s, 2s
    finally:
        await client.close()


async def test_get_portfolio_429_triggers_backoff(mock_stock_api, monkeypatch):
    """429 → 1s backoff → 200."""
    sleep_calls = []

    async def fake_sleep(s):
        sleep_calls.append(s)

    monkeypatch.setattr("asyncio.sleep", fake_sleep)

    mock_stock_api.get("/api/webai/portfolio").mock(
        side_effect=[
            httpx.Response(429, text="rate limit"),
            httpx.Response(
                200, json={"holdings": [], "cash": [], "summary": {}}
            ),
        ]
    )
    client = StockClient(BASE_URL, API_KEY)
    try:
        result = await client.get_portfolio()
        assert result["holdings"] == []
        assert sleep_calls == [1]
    finally:
        await client.close()


async def test_get_portfolio_falls_back_to_stale_on_all_failures(
    mock_stock_api, monkeypatch, caplog
):
    """cache 에 이전 성공 응답 + 모든 retry 5xx → stale 반환 + logger.warning."""
    # No-op sleep for fast test
    async def fake_sleep(s):
        return None
    monkeypatch.setattr("asyncio.sleep", fake_sleep)

    # Patch time.monotonic BEFORE first call so cached timestamp uses fake clock
    fake_time = [0.0]
    monkeypatch.setattr(
        "ai_trade.stock_client.time.monotonic", lambda: fake_time[0]
    )

    # First call succeeds
    route1 = mock_stock_api.get("/api/webai/portfolio").mock(
        return_value=httpx.Response(
            200,
            json={"holdings": [{"ticker": "005930"}], "cash": [], "summary": {}},
        )
    )
    client = StockClient(BASE_URL, API_KEY)
    try:
        first = await client.get_portfolio()
        assert first["holdings"][0]["ticker"] == "005930"

        # Advance fake clock past TTL (180s) so cache is stale
        fake_time[0] = 181.0

        # Now mock to return 500s persistently
        route1.mock(return_value=httpx.Response(500, text="server error"))

        with caplog.at_level(logging.WARNING, logger="ai_trade.stock_client"):
            result = await client.get_portfolio()
            assert result["holdings"][0]["ticker"] == "005930"  # stale data returned
            assert any(
                "stale" in rec.message.lower() for rec in caplog.records
            )
    finally:
        await client.close()
