import os, tempfile

def _fresh_db(monkeypatch):
    tmp = tempfile.mkdtemp()
    path = os.path.join(tmp, "lotto.db")
    from app import db
    monkeypatch.setattr(db, "DB_PATH", path)
    db.init_db()
    return db

def test_backtest_tables_exist(monkeypatch):
    db = _fresh_db(monkeypatch)
    with db._conn() as conn:
        tables = {r["name"] for r in conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'").fetchall()}
    assert "backtest_runs" in tables
    assert "winner_calibration" in tables

def test_backtest_runs_unique(monkeypatch):
    db = _fresh_db(monkeypatch)
    db.save_backtest_run(draw_no=100, strategy="random_null", weight_label="-",
                         weight_json=None, trial_id=None, n_tickets=10,
                         hist={"m3":1,"m4":0,"m5":0,"m6":0,"bonus_hits":0},
                         best_match=3, avg_meta_score=0.5)
    db.save_backtest_run(draw_no=100, strategy="random_null", weight_label="-",
                         weight_json=None, trial_id=None, n_tickets=10,
                         hist={"m3":2,"m4":0,"m5":0,"m6":0,"bonus_hits":0},
                         best_match=3, avg_meta_score=0.6)  # 멱등 upsert
    rows = db.get_backtest_runs(draw_no=100)
    assert len(rows) == 1
    assert rows[0]["m3"] == 2  # 마지막 값으로 갱신


_SCORES = {
    "score_total": 1.23,
    "score_frequency": 0.30,
    "score_fingerprint": 0.25,
    "score_gap": 0.20,
    "score_cooccur": 0.28,
    "score_diversity": 0.20,
}


def test_winner_calibration_upsert(monkeypatch):
    """save_winner_calibration 두 번 호출 시 upsert — 행 1개, 값은 마지막 것."""
    db = _fresh_db(monkeypatch)
    winning = [3, 7, 15, 22, 33, 41]
    db.save_winner_calibration(draw_no=200, winning=winning,
                               scores=_SCORES, percentile=75.0,
                               my_pick_avg=0.9, cache_draws=100)
    # 두 번째 저장 — percentile, my_pick_avg 업데이트
    scores2 = {**_SCORES, "score_total": 2.00}
    db.save_winner_calibration(draw_no=200, winning=winning,
                               scores=scores2, percentile=80.0,
                               my_pick_avg=1.1, cache_draws=110)
    row = db.get_winner_calibration(200)
    assert row is not None
    # 행이 1개만 존재하는지 확인
    with db._conn() as conn:
        cnt = conn.execute(
            "SELECT COUNT(*) AS c FROM winner_calibration WHERE draw_no=200"
        ).fetchone()["c"]
    assert cnt == 1
    assert row["percentile"] == 80.0
    assert row["score_total"] == 2.00


def _seed_draws(db, n=40):
    rows = []
    import random as _r; _r.seed(2)
    for i in range(1, n + 1):
        s = sorted(_r.sample(range(1, 46), 6))
        rows.append({"drw_no": i, "drw_date": f"2020-01-{(i%28)+1:02d}",
                     "n1": s[0], "n2": s[1], "n3": s[2], "n4": s[3],
                     "n5": s[4], "n6": s[5], "bonus": ((s[5] % 45) + 1)})
    db.upsert_many_draws(rows)

def test_backfill_calibration_idempotent(monkeypatch):
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    r1 = bt.backfill_calibration(batch=15, sample_m=200)
    # 첫 회차는 point-in-time 데이터가 빈약 → min_history 이후만 처리
    done1 = len(db.get_calibrated_draw_nos())
    assert done1 > 0
    r2 = bt.backfill_calibration(batch=100, sample_m=200)  # 나머지
    done2 = len(db.get_calibrated_draw_nos())
    assert done2 >= done1
    r3 = bt.backfill_calibration(batch=100, sample_m=200)  # 재실행 → 추가 0
    assert r3["calibrated"] == 0


def test_run_forward_purchase_persists_all_strategies(monkeypatch):
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    # 작은 규모로 빠르게
    res = bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
    assert res["ok"] is True
    rows = db.get_backtest_runs(draw_no=40)
    strategies = {r["strategy"] for r in rows}
    assert "random_null" in strategies
    assert "coverage" in strategies
    assert "engine_w" in strategies   # base 가중치로 최소 1건
    for r in rows:
        assert r["n_tickets"] == 20


def test_calibrate_winner_no_draw(monkeypatch):
    """DB에 없는 회차 번호 → ok=False, reason='no_draw'."""
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    r = bt.calibrate_winner(99999)
    assert r["ok"] is False
    assert r["reason"] == "no_draw"


def test_calibrate_winner_insufficient_history(monkeypatch):
    """point-in-time 이력이 MIN_HISTORY(30) 미만인 회차 → reason='insufficient_history'.
    draw_no=20이면 PIT 이력이 19개(draws 1~19)로 30 미만."""
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    r = bt.calibrate_winner(20)
    assert r["ok"] is False
    assert r["reason"] == "insufficient_history"


def test_run_forward_purchase_with_trials(monkeypatch):
    """그 주 weight_trials가 존재하면 engine_w 행의 weight_label이 'w0'..'w5' 형식이어야 한다."""
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    # draw 40: drw_date='2020-01-13' → week_start='2020-01-13'
    from datetime import date, timedelta
    draw_date = date.fromisoformat("2020-01-13")
    ws = (draw_date - timedelta(days=draw_date.weekday())).isoformat()
    # 해당 주에 trial 2개 심기 (day_of_week 0, 1)
    db.save_weight_trial(ws, 0, [0.1, 0.3, 0.2, 0.2, 0.2], "perturb")
    db.save_weight_trial(ws, 1, [0.25, 0.25, 0.25, 0.15, 0.1], "perturb")
    from app import backtest as bt
    res = bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
    assert res["ok"] is True
    rows = db.get_backtest_runs(draw_no=40)
    engine_w_labels = {r["weight_label"] for r in rows if r["strategy"] == "engine_w"}
    # trials가 있으므로 'base'가 아닌 'w0', 'w1' 형식이어야 한다
    assert "base" not in engine_w_labels
    assert any(lbl.startswith("w") for lbl in engine_w_labels)


def test_run_forward_purchase_idempotent(monkeypatch):
    """run_forward_purchase 두 번 호출 시 upsert — 행 수 변화 없음."""
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
    count_after_first = len(db.get_backtest_runs(draw_no=40))
    bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
    count_after_second = len(db.get_backtest_runs(draw_no=40))
    assert count_after_second == count_after_first


def test_get_calibrated_draw_nos(monkeypatch):
    """저장된 draw_no 집합이 get_calibrated_draw_nos에 포함되어야 한다."""
    db = _fresh_db(monkeypatch)
    winning = [1, 2, 3, 4, 5, 6]
    for draw_no in (301, 302, 303):
        db.save_winner_calibration(draw_no=draw_no, winning=winning,
                                   scores=_SCORES, percentile=50.0,
                                   my_pick_avg=0.5, cache_draws=50)
    nos = db.get_calibrated_draw_nos()
    assert isinstance(nos, set)
    assert {301, 302, 303}.issubset(nos)


def test_track_record_and_review_payload(monkeypatch):
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=5)
    bt.calibrate_winner(40, sample_m=200)

    tr = bt.track_record()
    assert "random_null" in tr["by_strategy"]
    # 이제 random_null은 N_NULL_TRIALS=6 행이므로 6*20=120장
    assert tr["by_strategy"]["random_null"]["n_tickets"] >= 20

    payload = bt.build_review_payload(40)
    assert payload["draw_no"] == 40
    assert "winner_analysis" in payload     # 당첨조합 5분석치
    assert "forward" in payload             # 이번 회차 전략별 성적
    assert "calibration_trend" in payload
    assert payload["winner_analysis"] is not None
    assert "score_total" in payload["winner_analysis"]


def test_run_forward_purchase_random_null_count(monkeypatch):
    """run_forward_purchase는 random_null을 N_NULL_TRIALS=6개 저장해야 한다."""
    db = _fresh_db(monkeypatch)
    _seed_draws(db, 40)
    from app import backtest as bt
    res = bt.run_forward_purchase(draw_no=40, k=20, pool_n=500, sample_seed=7)
    assert res["ok"] is True
    rows = db.get_backtest_runs(draw_no=40)
    null_rows = [r for r in rows if r["strategy"] == "random_null"]
    assert len(null_rows) == bt.N_NULL_TRIALS  # 6개
    null_labels = {r["weight_label"] for r in null_rows}
    assert null_labels == {f"r{i}" for i in range(bt.N_NULL_TRIALS)}
    for r in null_rows:
        assert r["n_tickets"] == 20


def test_evaluate_weekly_gated_keeps_base_unchanged(monkeypatch):
    """Fix 5 통합 테스트 (end-to-end gated path).

    접근: DB에 draws, weight_trials, auto_picks, backtest_runs, base_history를 직접 심어
    evaluate_weekly()의 gated 분기가 base를 바꾸지 않음을 검증한다.

    gated 조건: engine_w 최고 prize_score − random_best < LIFT_EPSILON(10.0).
    engine_best=5, random_best=20 → lift=-15 → gated.

    evaluate_weekly 내부 흐름:
      - get_weekly_trials(week_start) : _today_kst() 기준 week_start 사용
      - get_latest_draw()             : draws 테이블에서 max(drw_no) 반환
    두 참조가 같은 날짜 기준이어야 하므로 _today_kst를 monkeypatch로 고정하고
    draws의 최신 회차 날짜(drw_date)를 해당 주의 날짜로 맞춘다.
    """
    import json as _json
    from datetime import date, timedelta, datetime as _dt, timezone as _tz, timedelta as _td

    db = _fresh_db(monkeypatch)

    # KST 오늘 날짜 — evaluate_weekly가 이 날짜를 기준으로 week_start 계산
    KST = _tz(_td(hours=9))
    today_kst = _dt.now(KST).date()
    from app import weight_evolver as we
    week_start = we.get_week_start(today_kst)

    # 1) draws 심기 — 최신 회차의 drw_date를 week_start 주 안의 날짜로 맞춤
    import random as _r; _r.seed(99)
    rows = []
    for i in range(1, 41):
        s = sorted(_r.sample(range(1, 46), 6))
        # 마지막 회차(40)는 오늘 날짜 사용 (week_start 주 내)
        if i == 40:
            drw_date = today_kst.isoformat()
        else:
            drw_date = f"2020-01-{(i % 28) + 1:02d}"
        rows.append({
            "drw_no": i, "drw_date": drw_date,
            "n1": s[0], "n2": s[1], "n3": s[2],
            "n4": s[3], "n5": s[4], "n6": s[5],
            "bonus": (s[5] % 45) + 1,
        })
    db.upsert_many_draws(rows)
    latest = db.get_latest_draw()
    assert latest is not None
    assert latest["drw_date"] == today_kst.isoformat()

    # 2) weight trial 1개 심기 (day_of_week=0, week_start=오늘 주)
    trial_w = [0.2, 0.2, 0.2, 0.2, 0.2]
    db.save_weight_trial(week_start, 0, trial_w, "perturb")
    trial_rows = db.get_weekly_trials(week_start)
    assert len(trial_rows) == 1
    trial_id = trial_rows[0]["id"]

    # 3) auto_picks 1개 심기 (winning 번호와 2개 일치 → max_correct=2)
    winning6 = [latest["n1"], latest["n2"], latest["n3"],
                latest["n4"], latest["n5"], latest["n6"]]
    pick = winning6[:2] + [40, 41, 42, 43]
    db.save_auto_pick(trial_id, 1, pick, meta_score=0.5)

    # 4) backtest_runs: engine_w prize_score=5, random_null 6개 prize_score=20 (gated 확실)
    LOW_HIST = {"m3": 5, "m4": 0, "m5": 0, "m6": 0, "bonus_hits": 0}   # prize=5
    HIGH_HIST = {"m3": 20, "m4": 0, "m5": 0, "m6": 0, "bonus_hits": 0}  # prize=20
    draw_no = latest["drw_no"]
    db.save_backtest_run(
        draw_no=draw_no, strategy="engine_w", weight_label="w0",
        weight_json=_json.dumps(trial_w), trial_id=trial_id, n_tickets=20,
        hist=LOW_HIST, best_match=2, avg_meta_score=0.5,
    )
    from app import backtest as bt
    for i in range(bt.N_NULL_TRIALS):
        db.save_backtest_run(
            draw_no=draw_no, strategy="random_null", weight_label=f"r{i}",
            weight_json=None, trial_id=None, n_tickets=20,
            hist=HIGH_HIST, best_match=3, avg_meta_score=0.5,
        )

    # 5) current base 저장 (이전 주 월요일 effective_from)
    base_w = [0.2, 0.2, 0.2, 0.2, 0.2]
    prev_monday = (today_kst - timedelta(weeks=1, days=today_kst.weekday())).isoformat()
    db.save_base_history(
        effective_from=prev_monday,
        weight=base_w,
        source_trial_id=None,
        update_reason="cold_start",
        winner_score=None,
        winner_max_correct=None,
    )
    assert db.get_current_base() == base_w

    # 6) evaluate_weekly 호출 — _today_kst()를 monkeypatch로 오늘 날짜 고정
    monkeypatch.setattr(we, "_today_kst", lambda: today_kst)

    result = we.evaluate_weekly()

    assert result.get("ok") is True, f"evaluate_weekly 실패: {result}"

    # gated path 검증
    update_reason = result.get("update_reason", "")
    assert update_reason in ("unchanged_gated", "idempotent_skip"), (
        f"gated여야 하는데 reason='{update_reason}' — 게이팅 로직 깨짐"
    )

    # base가 바뀌지 않았는지 검증
    new_base = result.get("new_base")
    assert new_base == base_w, (
        f"gated인데 base가 변경됨: {new_base} != {base_w}"
    )
