"""Unit tests for ORBTradingEngine._record_trade and run_post_close."""
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import MagicMock, call

import pytest

from apps.orb_trader.engine import ORBTradingEngine
from apps.orb_trader.models import ORBPositionRow


def _make_position(
    *,
    ticker: str = "AAPL",
    direction: str = "long",
    entry_price: float = 100.0,
    shares: int = 10,
    stop_distance: float = 2.0,
) -> ORBPositionRow:
    return ORBPositionRow(
        session_id="test-session",
        date="2026-01-05",
        ticker=ticker,
        direction=direction,
        entry_price=entry_price,
        entry_time="2026-01-05T09:40:00",
        shares=shares,
        orb_high=entry_price * 1.01,
        orb_low=entry_price * 0.99,
        atr_at_entry=stop_distance / 0.75,
        stop_distance=stop_distance,
        current_stop=entry_price - stop_distance,
        peak_price=entry_price,
        rvol=2.0,
        composite_score=0.7,
        order_id="order-1",
    )


def _make_engine() -> ORBTradingEngine:
    params = SimpleNamespace(
        daily_budget_reset=True,
        drawdown_governor_threshold=None,
        drawdown_governor_min_scale=0.30,
        streak_sizing_win_bonus=None,
        streak_sizing_loss_penalty=None,
        streak_sizing_max=2.5,
        streak_sizing_min=0.5,
    )
    session = SimpleNamespace(
        session_id="test-session",
        session_name="test",
        initial_equity=10_000.0,
    )
    state = MagicMock()
    state.get_equity.return_value = 10_000.0
    state.get_peak_equity.return_value = 10_000.0
    state.list_trades.return_value = []

    engine = object.__new__(ORBTradingEngine)
    engine._session = session
    engine._params = params
    engine._state = state
    engine._log_callback = None
    engine._date_str = "2026-01-05"
    return engine


# ── _record_trade ─────────────────────────────────────────────────────────────

class TestRecordTrade:
    def test_long_profit_pnl(self):
        eng = _make_engine()
        pos = _make_position(direction="long", entry_price=100.0, shares=10)
        eng._record_trade(pos, exit_price=105.0, exit_time="T", exit_reason="close", equity=10_000.0)

        saved = eng._state.save_trade.call_args[0][0]
        assert saved.pnl == pytest.approx((105.0 - 100.0) * 10)

    def test_long_loss_pnl(self):
        eng = _make_engine()
        pos = _make_position(direction="long", entry_price=100.0, shares=10)
        eng._record_trade(pos, exit_price=96.0, exit_time="T", exit_reason="stop_loss", equity=10_000.0)

        saved = eng._state.save_trade.call_args[0][0]
        assert saved.pnl == pytest.approx((96.0 - 100.0) * 10)  # -40.0

    def test_short_profit_pnl(self):
        # Short: profit = entry - exit (price falls)
        eng = _make_engine()
        pos = _make_position(direction="short", entry_price=100.0, shares=10)
        eng._record_trade(pos, exit_price=92.0, exit_time="T", exit_reason="trailing_stop", equity=10_000.0)

        saved = eng._state.save_trade.call_args[0][0]
        assert saved.pnl == pytest.approx((100.0 - 92.0) * 10)  # +80.0

    def test_short_loss_pnl(self):
        # Short: loss = entry - exit when price rises
        eng = _make_engine()
        pos = _make_position(direction="short", entry_price=100.0, shares=10)
        eng._record_trade(pos, exit_price=104.0, exit_time="T", exit_reason="stop_loss", equity=10_000.0)

        saved = eng._state.save_trade.call_args[0][0]
        assert saved.pnl == pytest.approx((100.0 - 104.0) * 10)  # -40.0

    def test_r_multiple_long(self):
        # entry=100, exit=106, stop_distance=2 → pnl=60, risk=20 → R=3.0
        eng = _make_engine()
        pos = _make_position(direction="long", entry_price=100.0, shares=10, stop_distance=2.0)
        eng._record_trade(pos, exit_price=106.0, exit_time="T", exit_reason="close", equity=10_000.0)

        saved = eng._state.save_trade.call_args[0][0]
        assert saved.r_multiple == pytest.approx(3.0)

    def test_position_closed_in_db(self):
        eng = _make_engine()
        pos = _make_position(direction="long", entry_price=100.0, shares=10)
        eng._record_trade(pos, exit_price=105.0, exit_time="T", exit_reason="close", equity=10_000.0)

        eng._state.close_position_record.assert_called_once_with(
            "test-session", "2026-01-05", "AAPL"
        )

    def test_exit_reason_preserved(self):
        for reason in ("close", "stop_loss", "trailing_stop"):
            eng = _make_engine()
            pos = _make_position(direction="long", entry_price=100.0, shares=5)
            eng._record_trade(pos, exit_price=102.0, exit_time="T", exit_reason=reason, equity=10_000.0)
            saved = eng._state.save_trade.call_args[0][0]
            assert saved.exit_reason == reason


# ── run_post_close ────────────────────────────────────────────────────────────

class TestRunPostClose:
    def test_equity_accumulates_daily_pnl(self):
        eng = _make_engine()
        eng._state.get_equity.return_value = 10_200.0  # prev equity
        eng._state.list_trades.return_value = [
            {"date": "2026-01-05", "pnl": 300.0, "exit_reason": "close"},
        ]

        result = eng.run_post_close("2026-01-05")
        assert result["equity"] == pytest.approx(10_500.0)
        assert result["daily_pnl"] == pytest.approx(300.0)

    def test_stops_hit_counts_stop_and_trailing(self):
        eng = _make_engine()
        eng._state.get_equity.return_value = 9_500.0
        eng._state.list_trades.return_value = [
            {"date": "2026-01-05", "pnl": -200.0, "exit_reason": "stop_loss"},
            {"date": "2026-01-05", "pnl": 50.0, "exit_reason": "trailing_stop"},
            {"date": "2026-01-05", "pnl": 400.0, "exit_reason": "close"},
        ]

        result = eng.run_post_close("2026-01-05")
        assert result["stops_hit"] == 2
        assert result["trades"] == 3

    def test_only_today_trades_counted(self):
        eng = _make_engine()
        eng._state.list_trades.return_value = [
            {"date": "2026-01-04", "pnl": 500.0, "exit_reason": "close"},  # yesterday
            {"date": "2026-01-05", "pnl": 100.0, "exit_reason": "close"},  # today
        ]

        result = eng.run_post_close("2026-01-05")
        assert result["daily_pnl"] == pytest.approx(100.0)
        assert result["trades"] == 1

    def test_snapshot_saved(self):
        eng = _make_engine()
        eng._state.list_trades.return_value = [
            {"date": "2026-01-05", "pnl": 250.0, "exit_reason": "close"},
        ]

        eng.run_post_close("2026-01-05")
        eng._state.save_daily_snapshot.assert_called_once()
        snap = eng._state.save_daily_snapshot.call_args[0][0]
        assert snap.date == "2026-01-05"
        assert snap.daily_pnl == pytest.approx(250.0)
