"""
Tests for PaperExecutor.close_position() settlement payout.

Regression: the old code computed cash += position_cost * resolution, which
ignores direction — a winning BUY_NO (resolution = 0.0) paid out $0.

Correct settlement:
  BUY_YES: payout = shares * resolution
  BUY_NO:  payout = shares * (1 - resolution)
  pnl = payout - net_cost
"""
import asyncio

import pytest

from bot.executor import paper
from bot.executor.paper import PaperExecutor


class FakeDB:
    """Minimal Database stub for close_position()."""

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

    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))


def _close(direction: str, resolution: float):
    """Open one paper trade (size $100 @ 0.5 → 200 shares, net_cost $102)
    and settle it at `resolution`. Returns (pnl, executor, notifications)."""
    notifications: list[tuple] = []

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

    async def run():
        db = FakeDB({
            "mkt1": [{"direction": direction, "shares": 200.0, "net_cost": 102.0}],
        })
        ex = PaperExecutor(db=db, bankroll=1000.0)
        ex._portfolio.cash = 898.0           # 1000 - net_cost spent at entry
        ex._portfolio.positions["mkt1"] = 100.0  # size_usdc, as execute() stores it

        original = paper.telegram.trade_closed
        paper.telegram.trade_closed = fake_trade_closed
        try:
            pnl = await ex.close_position("mkt1", resolution, question="Test market?")
            await asyncio.sleep(0)  # let the notification task run
        finally:
            paper.telegram.trade_closed = original
        return pnl, ex, db

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


def test_buy_yes_wins():
    pnl, ex, db, notif = _close("BUY_YES", resolution=1.0)
    assert pnl == pytest.approx(200.0 - 102.0)   # payout = 200 * 1.0
    assert pnl > 0
    assert ex._portfolio.cash == pytest.approx(898.0 + 200.0)
    assert notif[0][1] > 0                        # Telegram reports a win


def test_buy_yes_loses():
    pnl, ex, db, notif = _close("BUY_YES", resolution=0.0)
    assert pnl == pytest.approx(-102.0)           # payout = 0
    assert pnl < 0
    assert ex._portfolio.cash == pytest.approx(898.0)
    assert notif[0][1] < 0                        # Telegram reports a loss


def test_buy_no_wins():
    pnl, ex, db, notif = _close("BUY_NO", resolution=0.0)
    assert pnl == pytest.approx(200.0 - 102.0)   # payout = 200 * (1 - 0.0)
    assert pnl > 0
    assert ex._portfolio.cash == pytest.approx(898.0 + 200.0)
    assert notif[0][1] > 0                        # win despite resolution = 0.0


def test_buy_no_loses():
    pnl, ex, db, notif = _close("BUY_NO", resolution=1.0)
    assert pnl == pytest.approx(-102.0)           # payout = 200 * (1 - 1.0) = 0
    assert pnl < 0
    assert ex._portfolio.cash == pytest.approx(898.0)
    assert notif[0][1] < 0                        # loss despite resolution = 1.0


def test_position_is_removed_and_persisted():
    pnl, ex, db, notif = _close("BUY_YES", resolution=1.0)
    assert "mkt1" not in ex._portfolio.positions
    assert db.closed == [("mkt1", "resolved", 1.0)]


def test_unknown_market_returns_none():
    async def run():
        ex = PaperExecutor(db=FakeDB({}), bankroll=1000.0)
        return await ex.close_position("nope", 1.0)
    assert asyncio.run(run()) is None


def test_db_failure_keeps_position_for_retry():
    """Regression: a DB error during close must not mutate the in-memory
    portfolio — otherwise the next resolution check skips the market
    (not in positions) and the DB row stays open forever."""

    class FailingDB(FakeDB):
        async def close_paper_position(self, market_id, reason="", resolution=None):
            raise RuntimeError("db down")

    async def run():
        db = FailingDB({
            "mkt1": [{"direction": "BUY_YES", "shares": 200.0, "net_cost": 102.0}],
        })
        ex = PaperExecutor(db=db, bankroll=1000.0)
        ex._portfolio.cash = 898.0
        ex._portfolio.positions["mkt1"] = 100.0
        with pytest.raises(RuntimeError):
            await ex.close_position("mkt1", 1.0)
        return ex

    ex = asyncio.run(run())
    assert ex._portfolio.positions == {"mkt1": 100.0}   # still open in memory
    assert ex._portfolio.cash == pytest.approx(898.0)   # payout not credited
