"""
Tests for the automatic market-resolution detector (Phase 2).

Covers:
  - PolymarketClient.get_market_resolution() parsing of real Gamma API shapes
    (resolved YES/NO, still open, UMA-disputed, ambiguous prices, 404, errors).
  - check_resolutions() in bot/main.py: a resolved market settles the open
    paper position via PaperExecutor.close_position() and persists
    close_reason='resolved' with the resolution value.
"""
import asyncio
import json

import httpx
import pytest

from bot.data.polymarket import PolymarketClient, MarketResolution
from bot.executor import paper
from bot.executor.paper import PaperExecutor
from bot.main import check_resolutions


# ─────────────────────────────────────────────────────────────────────────────
# get_market_resolution() — Gamma API response parsing
# ─────────────────────────────────────────────────────────────────────────────

class FakeResponse:
    def __init__(self, status_code: int, payload: dict | None = None):
        self.status_code = status_code
        self._payload = payload or {}

    def json(self):
        return self._payload

    def raise_for_status(self):
        if self.status_code >= 400:
            raise httpx.HTTPStatusError(
                f"HTTP {self.status_code}", request=None, response=None
            )


class FakeHTTPClient:
    def __init__(self, response):
        self._response = response
        self.requested_urls: list[str] = []

    async def get(self, url, **kwargs):
        self.requested_urls.append(url)
        if isinstance(self._response, Exception):
            raise self._response
        return self._response


def _resolution_for(response) -> MarketResolution | None:
    client = PolymarketClient()
    client._client = FakeHTTPClient(response)
    return asyncio.run(client.get_market_resolution("12345"))


def _gamma_market(closed: bool, yes_price: str, no_price: str,
                  uma_status: str | None = "resolved") -> dict:
    """Mirror the real Gamma /markets/{id} payload shape (observed 2026-06-11)."""
    m = {
        "id": "12345",
        "question": "Test market?",
        "closed": closed,
        "active": True,
        "outcomePrices": json.dumps([yes_price, no_price]),
        "closedTime": "2026-06-11 13:15:01+00" if closed else None,
        "umaEndDate": "2026-06-11T13:15:01Z" if closed else None,
        "endDate": "2026-06-11T13:00:00Z",
    }
    if uma_status is not None:
        m["umaResolutionStatus"] = uma_status
    return m


def test_resolution_no_won():
    res = _resolution_for(FakeResponse(200, _gamma_market(True, "0", "1")))
    assert res.resolved is True
    assert res.resolution == 0.0
    assert res.resolved_at is not None


def test_resolution_yes_won():
    res = _resolution_for(FakeResponse(200, _gamma_market(True, "1", "0")))
    assert res.resolved is True
    assert res.resolution == 1.0


def test_open_market_not_resolved():
    res = _resolution_for(FakeResponse(
        200, _gamma_market(False, "0.51", "0.49", uma_status=None)
    ))
    assert res.resolved is False
    assert res.resolution is None


def test_closed_but_uma_disputed_not_settled():
    res = _resolution_for(FakeResponse(
        200, _gamma_market(True, "0", "1", uma_status="disputed")
    ))
    assert res.resolved is False


def test_closed_with_ambiguous_prices_not_settled():
    res = _resolution_for(FakeResponse(200, _gamma_market(True, "0.6", "0.4")))
    assert res.resolved is False


def test_market_not_found_returns_none():
    assert _resolution_for(FakeResponse(404)) is None


def test_api_error_returns_none():
    assert _resolution_for(httpx.ConnectError("boom")) is None


# ─────────────────────────────────────────────────────────────────────────────
# check_resolutions() — detector loop settles paper positions
# ─────────────────────────────────────────────────────────────────────────────

class FakeDB:
    """Database stub: one open BUY_NO paper position."""

    def __init__(self, trades_by_market: dict[str, list[dict]]):
        self._trades = trades_by_market
        self.closed: list[tuple] = []

    async def get_open_position_details(self) -> list[dict]:
        return [
            {"market_id": mid, "question": t[0].get("question", ""),
             "direction": t[0]["direction"]}
            for mid, t in self._trades.items()
        ]

    async def get_open_trades_for_market(self, market_id: str) -> list[dict]:
        return self._trades.get(market_id, [])

    async def close_paper_position(self, market_id, reason="", resolution=None):
        self.closed.append((market_id, reason, resolution))


class FakePoly:
    def __init__(self, resolutions: dict[str, MarketResolution | None]):
        self._resolutions = resolutions
        self.checked: list[str] = []

    async def get_market_resolution(self, market_id: str):
        self.checked.append(market_id)
        return self._resolutions.get(market_id)


def _run_check(resolutions: dict, trades: dict):
    notifications: list[tuple] = []

    async def fake_trade_closed(question, pnl):
        notifications.append((question, pnl))

    async def run():
        db = FakeDB(trades)
        ex = PaperExecutor(db=db, bankroll=1000.0)
        for mid, t in trades.items():
            ex._portfolio.positions[mid] = sum(x["net_cost"] for x in t) - 2.0
        ex._portfolio.cash = 898.0
        poly = FakePoly(resolutions)

        original = paper.telegram.trade_closed
        paper.telegram.trade_closed = fake_trade_closed
        try:
            await check_resolutions(poly, ex, db)
            await asyncio.sleep(0)  # let notification task run
        finally:
            paper.telegram.trade_closed = original
        return db, ex, poly

    db, ex, poly = asyncio.run(run())
    return db, ex, poly, notifications


BUY_NO_TRADE = {
    "mkt1": [{
        "direction": "BUY_NO", "shares": 200.0, "net_cost": 102.0,
        "question": "Will X happen?",
    }],
}


def test_resolved_buy_no_position_is_closed():
    """BUY_NO position + market resolved NO (resolution=0.0) → winning close."""
    db, ex, poly, notif = _run_check(
        {"mkt1": MarketResolution(resolved=True, resolution=0.0)},
        BUY_NO_TRADE,
    )
    assert poly.checked == ["mkt1"]
    # close_paper_position called with close_reason='resolved' and the resolution
    assert db.closed == [("mkt1", "resolved", 0.0)]
    # Position removed and payout credited: 200 shares * (1 - 0.0) = $200
    assert "mkt1" not in ex._portfolio.positions
    assert ex._portfolio.cash == pytest.approx(898.0 + 200.0)
    # Telegram notified with positive pnl (200 - 102)
    assert notif == [("Will X happen?", pytest.approx(98.0))]


def test_unresolved_position_stays_open():
    db, ex, poly, notif = _run_check(
        {"mkt1": MarketResolution(resolved=False)},
        BUY_NO_TRADE,
    )
    assert poly.checked == ["mkt1"]
    assert db.closed == []
    assert "mkt1" in ex._portfolio.positions
    assert notif == []


def test_api_failure_leaves_position_open():
    db, ex, poly, notif = _run_check({"mkt1": None}, BUY_NO_TRADE)
    assert db.closed == []
    assert "mkt1" in ex._portfolio.positions
