#!/usr/bin/env python3
"""Integration test for ORB paper trading system.

Runs all 5 daily phases against MockORBBroker (no Alpaca API calls).
Tests:
  1. Multiple sessions with different budgets
  2. Session creation + budget persistence in SQLite
  3. ORB detection + candidate ranking (all 971 tickers at once)
  4. Breakout check + position entry (Oracle snapshot mock)
  5. Stop checks (sim_bar_minutes interval)
  6. EOD exit + P&L recording
  7. Post-close equity snapshot (compound returns)

Usage:
    python scripts/test_orb_paper.py
"""
from __future__ import annotations

import os
import sys
import tempfile
import traceback
from pathlib import Path
from unittest.mock import patch

# ── Repo root on path ─────────────────────────────────────────────────────────
ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(ROOT))
os.chdir(ROOT)

import datetime as dt
from zoneinfo import ZoneInfo

_ET = ZoneInfo("America/New_York")

# ── Minimal ORB config YAML ───────────────────────────────────────────────────

_TEST_CONFIG_YAML = """\
strategy_mode: orb
orb_strategy:
  orb_minutes: 10
  sim_bar_minutes: 90
  entry_direction: long_only
  order_timeout_minutes: 45
  min_price: 5.0
  min_avg_dollar_volume: 1000000
  min_atr_14: 0.10
  min_rvol: 0.1
  max_candidates: 10
  min_candidates_to_trade: 1
  weight_rvol: 0.60
  weight_gap: 0.25
  weight_dollar_vol: 0.15
  atr_stop_multiplier: 0.05
  breakeven_at_r: 1.0
  trailing_at_r: 10.0
  trailing_stop_atr_multiplier: 0.3
  risk_per_trade_pct: 0.01
  max_position_pct: 0.25
  daily_max_loss_pct: 0.05
  max_stops_per_day: 5
  exit_minutes_before_close: 5
universe:
  source: midlarge
"""

# Thresholds are intentionally loose so mock data always passes screening.

# ── Helpers ───────────────────────────────────────────────────────────────────

SEP = "─" * 60


def section(title: str) -> None:
    print(f"\n{SEP}")
    print(f"  {title}")
    print(SEP)


def ok(msg: str) -> None:
    print(f"  ✓ {msg}")


def fail(msg: str) -> None:
    print(f"  ✗ {msg}")
    sys.exit(1)


def assert_eq(label: str, got: Any, expected: Any) -> None:
    if got == expected:
        ok(f"{label}: {got!r}")
    else:
        fail(f"{label}: expected {expected!r}, got {got!r}")


from typing import Any


# ── Setup ─────────────────────────────────────────────────────────────────────

def setup_test_env(tmp_dir: str) -> tuple[str, str]:
    """Write test config and return (config_path, db_path)."""
    config_path = os.path.join(tmp_dir, "test_orb.yaml")
    db_path = os.path.join(tmp_dir, "orb_test.db")
    with open(config_path, "w") as f:
        f.write(_TEST_CONFIG_YAML)
    return config_path, db_path


# ── Phase runner ──────────────────────────────────────────────────────────────

def run_phases(
    engine: Any,
    broker: Any,
    date_str: str,
    session_name: str,
) -> dict[str, Any]:
    """Run all 5 engine phases and return phase results dict."""
    from apps.orb_trader.mock_broker import make_mock_snapshots

    tickers = broker._tickers

    results: dict[str, Any] = {}

    print(f"  [{session_name}] Phase 1: orb_detection")
    with patch("apps.orb_trader.engine.load_universe", return_value=tickers):
        r = engine.run_orb_detection(date_str)
    results["orb_detection"] = r
    print(f"    → {r}")

    print(f"  [{session_name}] Phase 2: breakout_check")
    mock_snaps = make_mock_snapshots(tickers, date_str, price_mult=1.05)
    with patch("libs.oracle_client.alpaca.get_snapshots", return_value=mock_snaps):
        r = engine.run_breakout_check(date_str)
    results["breakout_check"] = r
    print(f"    → {r}")

    print(f"  [{session_name}] Phase 3: stop_check")
    r = engine.run_stop_check(date_str)
    results["stop_check"] = r
    print(f"    → {r}")

    print(f"  [{session_name}] Phase 4: eod_exit")
    r = engine.run_eod_exit(date_str)
    results["eod_exit"] = r
    print(f"    → {r}")

    print(f"  [{session_name}] Phase 5: post_close")
    r = engine.run_post_close(date_str)
    results["post_close"] = r
    print(f"    → {r}")

    return results


# ── Main test ─────────────────────────────────────────────────────────────────

def main() -> None:
    print("\nORB Paper Trading — Integration Test")
    print("=" * 60)

    with tempfile.TemporaryDirectory() as tmp_dir:
        config_path, db_path = setup_test_env(tmp_dir)

        # ── 1. Session creation + budget tracking ─────────────────────────────
        section("1. Session creation + budget persistence")

        from apps.orb_trader.state import ORBStateManager

        state = ORBStateManager(db_path)

        SESSIONS = [
            ("alpha_session", 10_000.0),
            ("beta_session",  25_000.0),
        ]

        for name, capital in SESSIONS:
            sid = state.create_session(name, config_path, capital)
            ok(f"Created '{name}' (id={sid}, budget=${capital:,.0f})")

        sessions = state.list_sessions()
        assert_eq("session count", len(sessions), 2)

        for s in sessions:
            expected_budget = dict(SESSIONS)[s.session_name]
            assert_eq(
                f"  {s.session_name}.initial_equity",
                s.initial_equity,
                expected_budget,
            )

        # ── 2. Duplicate session name rejected ────────────────────────────────
        section("2. Duplicate name check")
        try:
            state.create_session("alpha_session", config_path, 999.0)
            fail("Should have raised IntegrityError for duplicate name")
        except Exception as exc:
            ok(f"Duplicate rejected: {type(exc).__name__}")

        # ── 3. Run all phases per session ─────────────────────────────────────
        section("3. Full trading day simulation (2 sessions)")

        # Use the most recent past trading day
        today = dt.date.today()
        # Find a weekday (Mon-Fri) that's in the past
        test_date = today - dt.timedelta(days=1)
        while test_date.weekday() >= 5:
            test_date -= dt.timedelta(days=1)
        date_str = test_date.isoformat()
        print(f"\n  Simulating trading day: {date_str}")

        from apps.orb_trader.mock_broker import MockORBBroker
        from apps.orb_trader.engine import make_orb_engine

        all_results: dict[str, dict] = {}

        for s in sessions:
            print(f"\n  {'─' * 40}")
            print(f"  Session: {s.session_name} | Budget: ${s.initial_equity:,.0f}")
            print(f"  {'─' * 40}")

            broker = MockORBBroker(
                initial_equity=s.initial_equity,
                tickers=["AAPL", "NVDA", "MSFT"],
            )
            engine = make_orb_engine(s, db_path, broker_override=broker)

            results = run_phases(engine, broker, date_str, s.session_name)
            all_results[s.session_name] = results

        # ── 4. Verify DB state after simulation ───────────────────────────────
        section("4. Verify DB state")

        for s in sessions:
            trades = state.list_trades(s.session_id)
            snapshots = state.list_snapshots(s.session_id)
            equity = state.get_equity(s.session_id)

            print(f"\n  [{s.session_name}]")
            ok(f"Trades recorded: {len(trades)}")
            ok(f"Equity snapshots: {len(snapshots)}")

            if snapshots:
                final_eq = snapshots[-1]["equity"]
                ok(f"Final equity: ${final_eq:,.2f} (initial: ${s.initial_equity:,.0f})")
            else:
                ok("No equity snapshot (no positions entered today — OK if no candidates)")

            assert_eq("equity from get_equity()", equity, snapshots[-1]["equity"] if snapshots else None)

        # ── 5. Verify budget independence between sessions ─────────────────────
        section("5. Budget independence check")

        eq_alpha = state.get_equity("alpha_session") or dict(SESSIONS)["alpha_session"]
        eq_beta  = state.get_equity("beta_session")  or dict(SESSIONS)["beta_session"]

        if eq_alpha != eq_beta:
            ok(f"Sessions have independent equity: alpha=${eq_alpha:,.2f}, beta=${eq_beta:,.2f}")
        else:
            ok(f"Sessions both at ${eq_alpha:,.2f} (no trades today — budgets remain independent)")

        # ── 6. Pause / resume / delete ────────────────────────────────────────
        section("6. Session lifecycle (pause / resume / delete)")

        alpha = state.get_session("alpha_session")
        state.set_session_status(alpha.session_id, "paused")
        refreshed = state.get_session("alpha_session")
        assert_eq("status after pause", refreshed.status, "paused")

        state.set_session_status(alpha.session_id, "active")
        refreshed = state.get_session("alpha_session")
        assert_eq("status after resume", refreshed.status, "active")

        state.delete_session(alpha.session_id)
        deleted = state.get_session("alpha_session")
        assert_eq("session after delete", deleted, None)
        ok("alpha_session deleted")

        remaining = state.list_sessions()
        assert_eq("remaining sessions", len(remaining), 1)
        assert_eq("remaining session name", remaining[0].session_name, "beta_session")

    print(f"\n{'=' * 60}")
    print("  ALL TESTS PASSED")
    print(f"{'=' * 60}\n")


if __name__ == "__main__":
    try:
        main()
    except SystemExit:
        raise
    except Exception:
        traceback.print_exc()
        sys.exit(1)
