"""Tests: AutoScheduler catchup re-runs missed phases after restart."""
from __future__ import annotations

import datetime as dt
from unittest.mock import AsyncMock, MagicMock, patch
from zoneinfo import ZoneInfo

import pytest


ET = ZoneInfo("America/New_York")


def _make_scheduler():
    from apps.web.paper_trading_service import AutoScheduler
    sched = AutoScheduler()
    sched._sessions = ["session_a"]
    sched._db_path = "/tmp/test.db"
    sched._log = lambda msg: None
    sched._dry_run = False
    return sched


@pytest.mark.asyncio
async def test_run_open_reruns_if_missed_and_not_in_phases():
    """If server restarts at 10 AM ET and run_open not in processed_phases, it must run."""
    sched = _make_scheduler()

    # now_et = 10:00 AM ET on a trading day
    now_et = dt.datetime(2026, 4, 24, 10, 0, 0, tzinfo=ET)

    mock_state = MagicMock()
    mock_session = MagicMock()
    mock_session.session_id = "sid-abc"
    mock_state.get_session.return_value = mock_session
    mock_state.is_phase_processed.return_value = False  # not yet run

    run_trading_calls = []

    async def fake_run_trading(cmd, sessions):
        run_trading_calls.append(cmd)

    with (
        patch.object(sched, "_now_et", return_value=now_et),
        patch.object(sched, "_is_trading_day", return_value=True),
        patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
        patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
        patch.object(sched, "_run_trading", side_effect=fake_run_trading),
        patch("apps.paper_trader.state.StateManager", return_value=mock_state),
    ):
        await sched._run_catchup()

    assert "run-open" in run_trading_calls


@pytest.mark.asyncio
async def test_run_open_not_rerun_if_already_in_phases():
    """If run_open already succeeded today (in processed_phases), skip it."""
    sched = _make_scheduler()

    now_et = dt.datetime(2026, 4, 24, 11, 0, 0, tzinfo=ET)

    mock_state = MagicMock()
    mock_session = MagicMock()
    mock_session.session_id = "sid-abc"
    mock_state.get_session.return_value = mock_session
    mock_state.is_phase_processed.return_value = True  # already ran

    run_trading_calls = []

    async def fake_run_trading(cmd, sessions):
        run_trading_calls.append(cmd)

    with (
        patch.object(sched, "_now_et", return_value=now_et),
        patch.object(sched, "_is_trading_day", return_value=True),
        patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
        patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
        patch.object(sched, "_run_trading", side_effect=fake_run_trading),
        patch("apps.paper_trader.state.StateManager", return_value=mock_state),
    ):
        await sched._run_catchup()

    assert "run-open" not in run_trading_calls


@pytest.mark.asyncio
async def test_run_close_reruns_if_missed():
    """If server restarts at 16:00 ET and run_close not in processed_phases, it must run."""
    sched = _make_scheduler()

    now_et = dt.datetime(2026, 4, 24, 16, 0, 0, tzinfo=ET)

    mock_state = MagicMock()
    mock_session = MagicMock()
    mock_session.session_id = "sid-abc"
    mock_state.get_session.return_value = mock_session
    # is_phase_processed: next_open=True (already ran), reaction_close=False (missed)
    def is_phase_processed(sid, date, phase):
        return phase == "next_open"
    mock_state.is_phase_processed.side_effect = is_phase_processed

    run_trading_calls = []

    async def fake_run_trading(cmd, sessions):
        run_trading_calls.append(cmd)

    with (
        patch.object(sched, "_now_et", return_value=now_et),
        patch.object(sched, "_is_trading_day", return_value=True),
        patch.object(sched, "_prev_trading_day", return_value=dt.date(2026, 4, 23)),
        patch.object(sched, "_run_pipeline", new_callable=AsyncMock, return_value=True),
        patch.object(sched, "_run_trading", side_effect=fake_run_trading),
        patch("apps.paper_trader.state.StateManager", return_value=mock_state),
    ):
        await sched._run_catchup()

    assert "run-close" in run_trading_calls
    # run_open should NOT re-run (market already closed)
    assert "run-open" not in run_trading_calls
