"""monitor.run_cycle — 게이트/필터/조립/격리."""
from monitor import MonitorState, filter_krx, run_cycle
from config import load_settings
from _shared.heartbeat import WorkerStats


def test_filter_krx_keeps_only_numeric6():
    targets = [{"ticker": "005930"}, {"ticker": "AAPL"}, {"ticker": "00660"},
               {"ticker": "000660"}, {"ticker": "0059301"}]
    kept = {t["ticker"] for t in filter_krx(targets)}
    assert kept == {"005930", "000660"}


class _FakeNAS:
    def __init__(self, ms):
        self._ms = ms
        self.reported = None

    async def get_monitor_set(self):
        return self._ms

    async def post_report(self, as_of, firing):
        self.reported = {"as_of": as_of, "firing": firing}
        return {"new_alerts": len(firing), "cleared": 0}


class _FakeKIS:
    def __init__(self, price=100, fail_on=None):
        self._price = price
        self._fail_on = fail_on or set()

    async def get_quote(self, ticker):
        if ticker in self._fail_on:
            raise RuntimeError("KIS down")
        return {"price": self._price, "day_open": 99, "day_high": 100,
                "today_volume": 1000, "as_of": "x"}

    async def get_daily_ohlcv(self, ticker, days=250):
        # 정배열 + 저가 근접 → ma20_pullback 발화 유도
        return [{"open": 90, "high": 90, "low": 90, "close": 90, "volume": 1}] * 200 \
            + [{"open": 100, "high": 100, "low": 100, "close": 100, "volume": 1}] * 20


async def test_closed_session_skips_kis():
    nas = _FakeNAS({"session": "closed"})
    state, stats = MonitorState(), WorkerStats()
    await run_cycle(nas, _FakeKIS(), state, stats, load_settings())
    assert state.session_state == "market_closed"
    assert nas.reported is None   # report도 안 함


async def test_non_krx_skipped_and_report_sent():
    nas = _FakeNAS({"session": "regular",
                    "buy_targets": [{"ticker": "AAPL", "name": "Apple"}],
                    "sell_targets": [], "buy_params": {}, "exit_params": {}})
    state, stats = MonitorState(), WorkerStats()
    await run_cycle(nas, _FakeKIS(), state, stats, load_settings())
    assert state.session_state == "market_open"
    assert nas.reported is not None
    assert nas.reported["firing"] == []   # 알파벳 티커 skip → 빈 발화


async def test_firing_assembled_and_last_alert_set():
    nas = _FakeNAS({"session": "regular",
                    "buy_targets": [{"ticker": "005930", "name": "삼성전자"}],
                    "sell_targets": [], "buy_params": {"pullback_pct": 0.02},
                    "exit_params": {}})
    state, stats = MonitorState(), WorkerStats()
    await run_cycle(nas, _FakeKIS(price=101), state, stats, load_settings())
    conds = {f["condition"] for f in nas.reported["firing"]}
    assert "buy_ma20_pullback" in conds
    assert state.last_alert_at is not None


async def test_per_ticker_failure_isolated():
    nas = _FakeNAS({"session": "regular",
                    "buy_targets": [{"ticker": "005930"}, {"ticker": "000660"}],
                    "sell_targets": [], "buy_params": {}, "exit_params": {}})
    state, stats = MonitorState(), WorkerStats()
    # 005930은 실패, 000660은 성공 → 루프가 죽지 않고 report 전송
    await run_cycle(nas, _FakeKIS(fail_on={"005930"}), state, stats, load_settings())
    assert nas.reported is not None
    assert state.session_state == "market_open"


async def test_monitor_set_failure_sets_idle():
    class _BadNAS(_FakeNAS):
        async def get_monitor_set(self):
            raise RuntimeError("NAS down")

    nas = _BadNAS({})
    state, stats = MonitorState(), WorkerStats()
    await run_cycle(nas, _FakeKIS(), state, stats, load_settings())
    assert state.session_state == "idle"
    assert nas.reported is None
