"""fetch_worker — 주간창/조립/격리/heartbeat."""
import datetime as dt
import json
from zoneinfo import ZoneInfo

from fetch_worker import FetchState, in_daytime, run_cycle, build_heartbeat
from config import load_settings

KST = ZoneInfo("Asia/Seoul")


def _kst(hh, mm):
    return dt.datetime(2026, 7, 10, hh, mm, tzinfo=KST)


def test_in_daytime_boundaries():
    assert in_daytime(_kst(8, 0), "08:00", "20:00") is True
    assert in_daytime(_kst(19, 59), "08:00", "20:00") is True
    assert in_daytime(_kst(20, 0), "08:00", "20:00") is False
    assert in_daytime(_kst(7, 59), "08:00", "20:00") is False


class _FakeNAS:
    def __init__(self, targets):
        self._targets = targets
        self.ingested = None

    async def get_targets(self):
        return self._targets

    async def post_ingest(self, fetched_at, batches):
        self.ingested = {"fetched_at": fetched_at, "batches": batches}
        return {"received": sum(len(b["articles"]) for b in batches), "new": 1, "matched": 1}


class _FakeNaver:
    def __init__(self, fail_dongs=None):
        self._fail = fail_dongs or set()
        self.token_acquired = False

    async def acquire_token(self):
        self.token_acquired = True

    async def fetch_articles(self, cortar_no, page=1):
        if cortar_no in self._fail:
            raise RuntimeError("naver 429")
        return {"isMoreData": False,
                "articleList": [{"articleNo": f"{cortar_no}-{page}"}]}


async def test_run_cycle_assembles_batches_and_ingests():
    nas = _FakeNAS({"dongs": [{"dong": "신대방동", "cortar_no": "1159010100"},
                              {"dong": "봉천동", "cortar_no": "1162010100"}],
                    "deal_types": ["매매"], "page_limit": 1})
    naver = _FakeNaver()
    state = FetchState()
    await run_cycle(nas, naver, state, load_settings())
    assert naver.token_acquired is True
    assert nas.ingested is not None
    dongs = [b["dong"] for b in nas.ingested["batches"]]
    assert dongs == ["신대방동", "봉천동"]
    assert state.listings_pushed == 2
    assert state.last_fetch_at is not None
    assert state.state == "idle"
    assert nas.ingested["fetched_at"].endswith("Z")


async def test_run_cycle_dong_failure_isolated():
    nas = _FakeNAS({"dongs": [{"dong": "A", "cortar_no": "1159010100"},
                              {"dong": "B", "cortar_no": "1162010100"}],
                    "page_limit": 1})
    naver = _FakeNaver(fail_dongs={"1159010100"})
    state = FetchState()
    await run_cycle(nas, naver, state, load_settings())
    assert nas.ingested is not None            # 실패해도 ingest 진행
    assert state.errors >= 1
    batches = {b["dong"]: b["articles"] for b in nas.ingested["batches"]}
    assert batches["A"] == []                   # 실패 동 빈 배열
    assert len(batches["B"]) == 1               # 성공 동


async def test_run_cycle_token_failure_skips_ingest():
    class _BadNaver(_FakeNaver):
        async def acquire_token(self):
            raise RuntimeError("token 실패")

    nas = _FakeNAS({"dongs": [], "page_limit": 1})
    state = FetchState()
    await run_cycle(nas, _BadNaver(), state, load_settings())
    assert nas.ingested is None
    assert state.errors == 1
    assert state.state == "idle"


def test_build_heartbeat_schema():
    state = FetchState()
    state.state = "fetching"
    state.listings_pushed = 7
    d = json.loads(build_heartbeat(state))
    assert d["name"] == "naver-fetch"
    assert d["kind"] == "fetcher"
    assert d["state"] == "fetching"
    assert isinstance(d["ts"], int)
    assert d["last_fetch_at"] is None
    assert d["listings_pushed"] == 7
    assert d["errors"] == 0
