"""美团、京东 CPS 每日自动对账 worker。"""
from __future__ import annotations

import logging
from datetime import date, datetime, timedelta

import pytest

from app.core import cps_reconcile_worker as worker
from app.core.config import settings
from app.core.rewards import CN_TZ
from app.db.session import SessionLocal
from app.integrations.meituan import MeituanCpsError


def _configure_platforms(monkeypatch) -> None:
    monkeypatch.setattr(settings, "MT_CPS_APP_KEY", "mt-key")
    monkeypatch.setattr(settings, "MT_CPS_APP_SECRET", "mt-secret")
    monkeypatch.setattr(settings, "JD_UNION_APP_KEY", "jd-key")
    monkeypatch.setattr(settings, "JD_UNION_APP_SECRET", "jd-secret")
    monkeypatch.setattr(settings, "CPS_AUTO_RECONCILE_LOOKBACK_DAYS", 3)


def test_reconcile_once_pulls_meituan_and_jd_by_update_time(monkeypatch) -> None:
    _configure_platforms(monkeypatch)
    calls: dict[str, dict] = {}

    def fake_meituan(db, **kwargs):
        calls["meituan"] = kwargs
        return {
            "fetched": 2,
            "inserted": 1,
            "updated": 1,
            "pages": 1,
            "api_requests": 1,
        }

    def fake_jd(db, **kwargs):
        calls["jd"] = kwargs
        return {
            "fetched": 3,
            "inserted": 2,
            "updated": 1,
            "pages": 2,
            "api_requests": 72,
            "windows": 72,
        }

    monkeypatch.setattr(worker.cps_repo, "reconcile_orders", fake_meituan)
    monkeypatch.setattr(worker.cps_repo, "reconcile_jd_orders", fake_jd)
    monkeypatch.setattr(worker, "_touch_lock", lambda: None)
    now = datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ)

    result = worker._reconcile_once(now)

    assert result["errors"] == {}
    assert result["meituan"]["fetched"] == 2
    assert result["jd"]["fetched"] == 3
    assert calls["meituan"]["query_time_type"] == 2
    assert calls["jd"]["query_time_type"] == 3
    assert calls["meituan"]["end_time"] == int(now.timestamp())
    assert calls["meituan"]["start_time"] == int((now - timedelta(days=3)).timestamp())
    assert calls["jd"]["start_time"] == now - timedelta(days=3)
    assert calls["jd"]["end_time"] == now
    assert calls["meituan"]["audit_context"]["run_id"] == result["run_id"]
    assert calls["jd"]["audit_context"]["run_id"] == result["run_id"]
    assert result["status"] == "success"
    assert result["manual_retry_required"] is False


def test_meituan_failure_does_not_block_jd(monkeypatch) -> None:
    _configure_platforms(monkeypatch)
    jd_called = False

    def fail_meituan(db, **kwargs):
        raise MeituanCpsError("temporary failure")

    def fake_jd(db, **kwargs):
        nonlocal jd_called
        jd_called = True
        return {"fetched": 1, "inserted": 1, "updated": 0, "pages": 1}

    monkeypatch.setattr(worker.cps_repo, "reconcile_orders", fail_meituan)
    monkeypatch.setattr(worker.cps_repo, "reconcile_jd_orders", fake_jd)
    monkeypatch.setattr(worker, "_touch_lock", lambda: None)

    result = worker._reconcile_once(datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ))

    assert result["errors"]["meituan"] == "temporary failure"
    assert jd_called is True
    assert result["jd"]["fetched"] == 1
    assert result["meituan"]["status"] == "failed"
    assert result["status"] == "partial_success"
    assert result["manual_retry_required"] is True


def test_should_run_once_after_five_am() -> None:
    before = datetime(2026, 7, 22, 4, 59, tzinfo=CN_TZ)
    at_five = datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ)

    assert worker._should_run(None, before, 5) is False
    assert worker._should_run(None, at_five, 5) is True
    assert worker._should_run(date(2026, 7, 22), at_five, 5) is False


def test_trigger_and_next_run_are_beijing_time() -> None:
    before_five = datetime(2026, 7, 22, 4, 30, tzinfo=CN_TZ)
    after_five = datetime(2026, 7, 22, 6, 0, tzinfo=CN_TZ)

    assert worker._trigger_for_run(before_five, after_five, 5) == "scheduled"
    assert worker._trigger_for_run(after_five, after_five, 5) == "startup_catchup"
    assert worker._next_run_at(before_five, 5) == datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ)
    assert worker._next_run_at(after_five, 5) == datetime(2026, 7, 23, 5, 0, tzinfo=CN_TZ)


def test_reconcile_emits_structured_audit_logs(monkeypatch, caplog) -> None:
    _configure_platforms(monkeypatch)
    monkeypatch.setattr(
        worker.cps_repo,
        "reconcile_orders",
        lambda db, **kwargs: {
            "fetched": 2,
            "inserted": 1,
            "updated": 1,
            "pages": 1,
            "api_requests": 1,
        },
    )
    monkeypatch.setattr(
        worker.cps_repo,
        "reconcile_jd_orders",
        lambda db, **kwargs: {
            "fetched": 3,
            "inserted": 2,
            "updated": 1,
            "pages": 1,
            "api_requests": 72,
            "windows": 72,
        },
    )
    monkeypatch.setattr(worker, "_touch_lock", lambda: None)
    monkeypatch.setattr(
        worker,
        "_cn_now",
        lambda: datetime(2026, 7, 22, 5, 1, tzinfo=CN_TZ),
    )

    with caplog.at_level(logging.INFO, logger=worker.logger.name):
        result = worker._reconcile_once(
            datetime(2026, 7, 22, 5, 0, tzinfo=CN_TZ),
            run_id="audit-run-1",
            trigger="scheduled",
        )

    records = {record.event: record for record in caplog.records if hasattr(record, "event")}
    assert records["cps_reconcile.started"].run_id == "audit-run-1"
    assert records["cps_reconcile.started"].lookback_days == 3
    assert records["cps_reconcile.started"].scheduled_date == "2026-07-22"
    assert records["cps_reconcile.started"].hostname
    assert records["cps_reconcile.completed"].task_status == "success"
    assert records["cps_reconcile.completed"].manual_retry_required is False
    assert records["cps_reconcile.completed"].meituan_result["fetched"] == 2
    assert records["cps_reconcile.completed"].jd_result["api_requests"] == 72
    assert result["next_run_at"] == "2026-07-23T05:00:00+08:00"


def test_jd_request_failure_logs_exact_window(monkeypatch, caplog) -> None:
    start = datetime(2026, 7, 20, 5, 0, tzinfo=CN_TZ)

    def fail_query(**kwargs):
        raise RuntimeError("upstream timeout")

    monkeypatch.setattr(worker.cps_repo.jd_union, "query_order_rows", fail_query)
    with SessionLocal() as db, caplog.at_level(logging.ERROR, logger=worker.logger.name):
        with pytest.raises(RuntimeError, match="upstream timeout"):
            worker.cps_repo.reconcile_jd_orders(
                db,
                start_time=start,
                end_time=start + timedelta(hours=2),
                audit_context={"run_id": "audit-run-2", "trigger": "scheduled"},
            )

    record = next(
        item
        for item in caplog.records
        if getattr(item, "event", "") == "cps_reconcile.request_failed"
    )
    assert record.run_id == "audit-run-2"
    assert record.platform == "jd"
    assert record.failed_window_start == "2026-07-20T05:00:00+08:00"
    assert record.failed_window_end == "2026-07-20T06:00:00+08:00"
    assert record.failed_page == 1


def test_start_worker_respects_auto_switch(monkeypatch) -> None:
    monkeypatch.setattr(settings, "CPS_AUTO_RECONCILE_ENABLED", False)
    assert worker.start_cps_reconcile_worker() is None
