"""Flashcard-Übungspool: Leitner-Schritte, Deck-Bau (Level-Gate, fällig/neu), Persistenz."""

import json
from datetime import datetime, timedelta, timezone

from learning import LEITNER_MAX_BOX, PRACTICE_NEW_PER_SESSION, leitner_step

TOPIC = "t"


def _iso(days: float = 0) -> str:
    return (datetime.now(timezone.utc) + timedelta(days=days)).isoformat()


async def _card(db, bn, sn, sub_title="Sub", q="Q?", block="Block"):
    await db.put_sub_artifact(TOPIC, bn, sn, "flashcard",
                              json.dumps({"question": q, "answer": "A"}), block, sub_title)


# ── Leitner rein funktional ──────────────────────────────────────────────────────────

def test_leitner_step_transitions():
    assert leitner_step(None, True) == (2, 1)    # neue Karte gewusst → Box 2, morgen
    assert leitner_step(None, False) == (1, 0)   # neue Karte falsch → Box 1, sofort
    assert leitner_step(2, True) == (3, 3)
    assert leitner_step(LEITNER_MAX_BOX, True) == (LEITNER_MAX_BOX, 21)  # Cap
    assert leitner_step(4, False) == (1, 0)      # falsch → zurück auf Anfang


# ── Persistenz ───────────────────────────────────────────────────────────────────────

async def test_progress_upsert_roundtrip(testdb):
    db = testdb
    await db.upsert_practice_progress(TOPIC, "b", "s", 2, _iso(1))
    await db.upsert_practice_progress(TOPIC, "b", "s", 3, _iso(3))
    rows = await db.get_practice_progress(TOPIC)
    assert len(rows) == 1 and rows[0]["box"] == 3


async def test_progress_survives_artefakte_wipe(testdb):
    db = testdb
    await _card(db, "b", "s")
    await db.upsert_practice_progress(TOPIC, "b", "s", 4, _iso(7))
    await db.delete_sub_artefakte(TOPIC)  # Regenerations-Wipe
    assert (await db.get_practice_progress(TOPIC))[0]["box"] == 4


async def test_delete_topic_pipeline_clears_progress(testdb):
    db = testdb
    await db.upsert_practice_progress(TOPIC, "b", "s", 2, _iso(1))
    await db.delete_topic_pipeline(TOPIC)
    assert await db.get_practice_progress(TOPIC) == []


async def test_sub_levels_norm_and_counts(testdb):
    db = testdb
    await db.put_subblock(TOPIC, "b", "s1", "Block", "S1", level="beginner")
    await db.put_subblock(TOPIC, "b", "s2", "Block", "S2", level="expert")
    await db.put_subblock(TOPIC, "b", "s3", "Block", "S3", level="beginner", relevance="peripheral")
    await db.put_subblock(TOPIC, "b", "s4", "Block", "S4", level="beginner", status="variant")
    levels = await db.sub_levels_norm(TOPIC)
    assert levels[("b", "s1")] == 1 and levels[("b", "s2")] == 3 and levels[("b", "s3")] == 4
    assert ("b", "s4") not in levels  # non-consensus ausgeschlossen
    counts = await db.subs_per_level_norm(TOPIC)
    assert counts["b"] == {1: 1, 2: 0, 3: 1, 4: 1}


# ── Deck-Bau ─────────────────────────────────────────────────────────────────────────

async def test_deck_level_gate_and_unlock(testdb):
    from routes import build_practice_deck
    db = testdb
    # block_norm muss _norm_title(Roh-Titel) sein — so entsteht er auch in der Pipeline
    await db.put_subblock(TOPIC, "block", "s1", "Block", "S1", level="beginner")
    await db.put_subblock(TOPIC, "block", "s2", "Block", "S2", level="expert")
    await _card(db, "block", "s1", "S1")
    await _card(db, "block", "s2", "S2")
    deck = await build_practice_deck(TOPIC)
    assert [c["sub_norm"] for c in deck["cards"]] == ["s1"]  # expert gesperrt
    assert deck["counts"]["gesperrt"] == 1
    # Score über S1+S2-Schwelle (2 Subs × 25 = 50) → expert (Level 3) frei
    await db.set_block_score_and_streak(TOPIC, "Block", 50, 0)
    deck = await build_practice_deck(TOPIC)
    assert {c["sub_norm"] for c in deck["cards"]} == {"s1", "s2"}


async def test_deck_due_before_new_oldest_first(testdb):
    from routes import build_practice_deck
    db = testdb
    for sn in ("s1", "s2", "s3"):
        await db.put_subblock(TOPIC, "b", sn, "Block", sn.upper(), level="beginner")
        await _card(db, "b", sn, sn.upper())
    await db.upsert_practice_progress(TOPIC, "b", "s2", 2, _iso(-1))
    await db.upsert_practice_progress(TOPIC, "b", "s3", 2, _iso(-5))
    deck = await build_practice_deck(TOPIC)
    assert [c["sub_norm"] for c in deck["cards"]] == ["s3", "s2", "s1"]  # älteste fällige zuerst
    assert [c["status"] for c in deck["cards"]] == ["due", "due", "new"]
    assert deck["counts"] == {"due": 2, "new": 1, "new_total": 1, "gesperrt": 0}


async def test_deck_caps_new_and_reports_total(testdb):
    from routes import build_practice_deck
    db = testdb
    for i in range(PRACTICE_NEW_PER_SESSION + 5):
        sn = f"s{i:02d}"
        await db.put_subblock(TOPIC, "b", sn, "Block", sn, level="beginner")
        await _card(db, "b", sn, sn)
    deck = await build_practice_deck(TOPIC)
    assert deck["counts"]["new"] == PRACTICE_NEW_PER_SESSION
    assert deck["counts"]["new_total"] == PRACTICE_NEW_PER_SESSION + 5


async def test_deck_future_due_sets_next_due_at(testdb):
    from routes import build_practice_deck
    db = testdb
    await db.put_subblock(TOPIC, "b", "s1", "Block", "S1", level="beginner")
    await _card(db, "b", "s1", "S1")
    await db.upsert_practice_progress(TOPIC, "b", "s1", 3, _iso(3))
    deck = await build_practice_deck(TOPIC)
    assert deck["cards"] == [] and deck["counts"]["due"] == 0
    assert deck["next_due_at"] is not None


async def test_deck_orphan_progress_and_legacy_block(testdb):
    from routes import build_practice_deck
    db = testdb
    # Orphan: Progress ohne Karte → unschädlich, taucht nicht auf
    await db.upsert_practice_progress(TOPIC, "weg", "s0", 2, _iso(-1))
    # Legacy: Karte ohne subblocks-Zeilen → ungefiltert durchlassen
    await _card(db, "leg", "sx", "SX")
    deck = await build_practice_deck(TOPIC)
    assert [c["block_norm"] for c in deck["cards"]] == ["leg"]


async def test_answer_books_without_card(testdb):
    """Antwort während Regeneration: bucht immer, kein Fehlerpfad."""
    from models import PracticeAnswerRequest
    from routes import practice_answer
    db = testdb
    res = await practice_answer(PracticeAnswerRequest(
        topic=TOPIC, block_norm="b", sub_norm="s", correct=True))
    assert res["box"] == 2
    res = await practice_answer(PracticeAnswerRequest(
        topic=TOPIC, block_norm="b", sub_norm="s", correct=False))
    assert res["box"] == 1
    assert (await db.get_practice_progress(TOPIC))[0]["box"] == 1
