"""Cross-Block-Konsolidierung (board_artefacts._proc_konsolidierung) und Finalize —
Judges gefaked, gegen Test-DB. Die In-Block-Konsolidierung lebt seit dem Verschmelzungs-
Umbau in block_calls._verify_block und wird in tests/test_block_calls.py getestet."""

import json

import numpy as np

import blocks
import board_artefacts as ba
from kanban import Flow
from pipeline import FAILED, OK, GenContext

TOPIC = "konsolidierung"


def _ctx():
    return GenContext(topic=TOPIC, provider="test", is_cancelled=lambda: False)


def _fake_slot(antworten):
    """run_single_slot-Fake: pro Judge-Key eine Antwort; schreibt via payload (wie der Engine-Sink)."""
    calls = []

    async def fake(ctx, label, *, key, prompt, role, capabilities, payload, timeout):
        calls.append({"key": key, "prompt": prompt})
        j = key.rsplit("-", 1)[-1]  # "j1"/"j2"
        antwort = antworten.get(j)
        if antwort is None:
            return FAILED, None
        return OK, payload((0, json.dumps(antwort), ""))

    fake.calls = calls
    return fake


async def _seed_block(db, bnorm, subs):
    for s in subs:
        await db.put_subblock(TOPIC, bnorm, blocks._norm_title(s), bnorm.title(), s, status="consensus")


def test_luecken_schnitt_cap():
    l1 = [f"Aspekt-{k} fehlt" for k in ("eins", "zwei", "drei", "vier", "fünf")]
    assert blocks._luecken_schnitt(l1, list(l1)) == l1[:3]  # Cap 3
    assert blocks._luecken_schnitt(["Inline-HTML"], ["Tabellen-Syntax"]) == []


def test_neg_set_lemmatisiert():
    """kein/keine/keinen falten auf einen Stamm; nicht vs. ohne bleiben verschieden."""
    a = blocks._neg_set("Fehlerverhalten (kein Syntaxfehler)")
    b = blocks._neg_set("Fehlerverhalten (keine Syntax-Fehlermeldung)")
    assert a == b == frozenset({"kein"})
    assert blocks._neg_set("nicht expandiert") != blocks._neg_set("ohne Expansion")
    assert blocks._neg_set("niemals gerendert") == blocks._neg_set("nie gerendert")


async def test_finalize_purges_stale_rows(testdb, tmp_path):
    """Re-Run-Waisen: Finalize löscht Alt-Fragen/-Artefakte des Blocks vor dem Upsert."""
    db = testdb
    await db.upsert_question_pattern(TOPIC, "alpha", "alt-sub", "Alpha", "Alt", "Alte Frage?")
    await db.put_sub_artifact(TOPIC, "alpha", "alt-sub", "flashcard", "{}", "Alpha", "Alt")
    await db.kanban_upsert_card(TOPIC, "artefacts", "alpha", "ablock", "finalize", {})
    files = {k: tmp_path / f"{k}.json" for k in
             ("sub_roh", "facts", "sidecar", "question_pattern", "artefakte")}
    card = {"card_id": "alpha", "payload": {
        "title": "Alpha", "raw": {"Alpha": ["Neu"]}, "facts": {},
        "sidecar": {"Alpha": [{"title": "Neu", "level": "beginner"}]},
        "pattern": {"Alpha": [{"subblock": "Neu", "question": "F?"}]},
        "artefacts": {"flashcard": [{"block": "Alpha", "subblock": "Neu", "front": "F", "back": "B"}]}}}
    flow = Flow(TOPIC, work_dir=tmp_path)
    await ba._proc_finalize(_ctx(), flow, files, [card])
    assert {r["sub_norm"] for r in await db.list_question_pattern(TOPIC)} == {"neu"}
    assert {(r["sub_norm"], r["type"]) for r in await db.get_sub_artefakte(TOPIC)} == {("neu", "flashcard")}


# ── Cross-Block ─────────────────────────────────────────────────────────────────────

class _FakeEmb:
    """Gleicher Text → gleicher Einheitsvektor, sonst orthogonal (cos 1.0 / 0.0)."""

    @staticmethod
    def available():
        return True

    @staticmethod
    def embed_sims(texts):
        uniq = {t: k for k, t in enumerate(dict.fromkeys(texts))}
        arr = np.zeros((len(texts), max(len(uniq), 1)))
        for r, t in enumerate(texts):
            arr[r, uniq[t]] = 1.0
        return arr @ arr.T


async def _cross_env(db, tmp_path, finalisiert=True):
    """Zwei finalisierte Karten in der End-Barriere; die Sub-Rows liegen in der DB
    (post-finalize ist die DB die Wahrheit, nicht mehr das Karten-Payload)."""
    flow = Flow(TOPIC, work_dir=tmp_path)
    cards = []
    for bnorm, subs in (("alpha", ["Gleiche Aussage", "Nur in Alpha"]),
                        ("beta", ["Gleiche Aussage", "Nur in Beta"])):
        payload = {"title": bnorm.title()}
        if finalisiert:
            payload.update(pattern={}, artefacts={})
        await db.kanban_upsert_card(TOPIC, "artefacts", bnorm, "ablock", "konsolidierung", payload)
        await _seed_block(db, bnorm, subs)
        cards.append({"card_id": bnorm, "payload": payload})
    files = {k: tmp_path / f"{k}.json" for k in
             ("sub_roh", "facts", "sidecar", "question_pattern", "artefakte")}
    return flow, cards, files


async def test_crossblock_folds_loser(testdb, tmp_path, monkeypatch):
    """Einstimmig „a" → Betas geteilte Aussage wird variant, ihre Frage wandert zum
    Gewinner (falte_sub), Karten gehen auf DONE."""
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path)
    sn = blocks._norm_title("Gleiche Aussage")
    await db.upsert_question_pattern(TOPIC, "beta", sn, "Beta", "Gleiche Aussage", "F?")
    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    fake = _fake_slot({"j1": {"pairs": {"1": "a"}}, "j2": {"pairs": {"1": "a"}}})
    monkeypatch.setattr(ba, "run_single_slot", fake)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    assert "Gleiche Aussage" in fake.calls[0]["prompt"]
    for cid in ("alpha", "beta"):
        assert (await db.kanban_get_card(TOPIC, "artefacts", cid))["stage"] == ba.DONE
    beta_rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert beta_rows[sn] == "variant"
    alpha_rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "alpha")}
    assert alpha_rows[sn] == "consensus"
    fragen = await db.list_question_pattern(TOPIC)
    assert {(r["block_norm"], r["sub_norm"]) for r in fragen} == {("alpha", sn)}  # umgehängt


async def test_crossblock_tiebreaker_folds(testdb, tmp_path, monkeypatch):
    """j1/j2 uneinig → j3 entscheidet mit Mehrheit; hier „a" → Beta verliert."""
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path)
    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    fake = _fake_slot({"j1": {"pairs": {"1": "a"}}, "j2": {"pairs": {"1": "nein"}},
                       "j3": {"pairs": {"1": "a"}}})
    monkeypatch.setattr(ba, "run_single_slot", fake)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    assert len(fake.calls) == 3
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert rows[blocks._norm_title("Gleiche Aussage")] == "variant"


async def test_crossblock_dissent_without_tiebreaker_keeps_both(testdb, tmp_path, monkeypatch):
    """j3 liefert nichts (FAILED) → fail-open, Paar bleibt."""
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path)
    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    fake = _fake_slot({"j1": {"pairs": {"1": "a"}}, "j2": {"pairs": {"1": "b"}}})  # j3 fehlt → FAILED
    monkeypatch.setattr(ba, "run_single_slot", fake)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    assert (await db.kanban_get_card(TOPIC, "artefacts", "beta"))["stage"] == ba.DONE
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert rows[blocks._norm_title("Gleiche Aussage")] == "consensus"


async def test_crossblock_ersatzrichter(testdb, tmp_path, monkeypatch):
    """Nur ein Richter liefert → Ersatz jE als zweite Stimme; Einstimmigkeit faltet."""
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path)
    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    fake = _fake_slot({"j1": {"pairs": {"1": "a"}}, "jE": {"pairs": {"1": "a"}}})  # j2 → FAILED
    monkeypatch.setattr(ba, "run_single_slot", fake)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert rows[blocks._norm_title("Gleiche Aussage")] == "variant"


async def test_crossblock_without_embedding_advances(testdb, tmp_path, monkeypatch):
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path)

    class _Aus:
        @staticmethod
        def available():
            return False

    async def kein_agent(*a, **kw):
        raise AssertionError("ohne Embedding kein Judge")

    monkeypatch.setattr(ba, "embedding", _Aus)
    monkeypatch.setattr(ba, "run_single_slot", kein_agent)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    for cid in ("alpha", "beta"):
        assert (await db.kanban_get_card(TOPIC, "artefacts", cid))["stage"] == ba.DONE


async def test_crossblock_nachzuegler_zurueck_zum_erzeugen(testdb, tmp_path, monkeypatch):
    """Resume-Karte ohne pattern im Payload → zurück nach generate (bzw. artefakte bei
    vorhandenem sidecar), KEIN Dedup — finalize würde den Fold sonst re-spiegeln."""
    db = testdb
    flow, cards, files = await _cross_env(db, tmp_path, finalisiert=False)
    cards[1]["payload"]["sidecar"] = {"Beta": []}  # hat Verify schon hinter sich
    await db.kanban_set_payload(TOPIC, "artefacts", "beta", cards[1]["payload"])

    async def kein_agent(*a, **kw):
        raise AssertionError("Nachzügler dürfen keinen Dedup auslösen")

    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    monkeypatch.setattr(ba, "run_single_slot", kein_agent)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    assert (await db.kanban_get_card(TOPIC, "artefacts", "alpha"))["stage"] == "generate"
    assert (await db.kanban_get_card(TOPIC, "artefacts", "beta"))["stage"] == "artefakte"
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert rows[blocks._norm_title("Gleiche Aussage")] == "consensus"


async def test_finalize_defaultet_level_nachzuegler(testdb, tmp_path):
    """Finalize klassifiziert level-/relevance-lose consensus-Rows (Default advanced/relevant)."""
    db = testdb
    await db.put_subblock(TOPIC, "alpha", "nachzuegler", "Alpha", "Nachzügler", status="consensus")
    await db.kanban_upsert_card(TOPIC, "artefacts", "alpha", "ablock", "finalize", {})
    files = {k: tmp_path / f"{k}.json" for k in
             ("sub_roh", "facts", "sidecar", "question_pattern", "artefakte")}
    card = {"card_id": "alpha", "payload": {"title": "Alpha", "raw": {}, "facts": {},
                                            "sidecar": {}, "pattern": {}, "artefacts": {}}}
    await ba._proc_finalize(_ctx(), Flow(TOPIC, work_dir=tmp_path), files, [card])
    row = next(r for r in await db.list_subblocks(TOPIC, "alpha"))
    assert row["level"] == "advanced" and row["relevance"] == "relevant"


async def test_finalize_loescht_stale_consensus(testdb, tmp_path):
    """Alt-consensus-Rows, die der Lauf-Sidecar nicht mehr trägt, fliegen raus —
    variant-Rows bleiben (QA liest die Status). Wurzel der 25 Board-2-losen Waisen."""
    db = testdb
    await db.put_subblock(TOPIC, "alpha", "alt-rest", "Alpha", "Alt-Rest", status="consensus")
    await db.put_subblock(TOPIC, "alpha", "alte-variante", "Alpha", "Alte Variante", status="variant")
    await db.kanban_upsert_card(TOPIC, "artefacts", "alpha", "ablock", "finalize", {})
    files = {k: tmp_path / f"{k}.json" for k in
             ("sub_roh", "facts", "sidecar", "question_pattern", "artefakte")}
    card = {"card_id": "alpha", "payload": {
        "title": "Alpha", "raw": {"Alpha": ["Neu"]}, "facts": {},
        "sidecar": {"Alpha": [{"title": "Neu", "level": "beginner"}]},
        "pattern": {}, "artefacts": {}}}
    await ba._proc_finalize(_ctx(), Flow(TOPIC, work_dir=tmp_path), files, [card])
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "alpha")}
    assert rows == {"neu": "consensus", "alte-variante": "variant"}


async def test_crossblock_chunking_faltet_global(testdb, tmp_path, monkeypatch):
    """Paare werden gechunkt beurteilt (ein Hänger blockiert nur noch seinen Chunk);
    die Verdicts falten global über alle Chunks."""
    db = testdb
    flow = Flow(TOPIC, work_dir=tmp_path)
    cards = []
    for bnorm, subs in (("alpha", ["Gleiche Aussage", "Zweite gleiche Aussage", "Nur in Alpha"]),
                        ("beta", ["Gleiche Aussage", "Zweite gleiche Aussage"])):
        payload = {"title": bnorm.title(), "pattern": {}, "artefacts": {}}
        await db.kanban_upsert_card(TOPIC, "artefacts", bnorm, "ablock", "konsolidierung", payload)
        await _seed_block(db, bnorm, subs)
        cards.append({"card_id": bnorm, "payload": payload})
    files = {k: tmp_path / f"{k}.json" for k in
             ("sub_roh", "facts", "sidecar", "question_pattern", "artefakte")}
    monkeypatch.setattr(ba, "embedding", _FakeEmb)
    monkeypatch.setattr(ba, "CROSS_CHUNK_PAARE", 1)  # 2 Paare → 2 Chunks
    fake = _fake_slot({"j1": {"pairs": {"1": "a"}}, "j2": {"pairs": {"1": "a"}}})
    monkeypatch.setattr(ba, "run_single_slot", fake)
    await ba._proc_konsolidierung(_ctx(), flow, files, "", cards)
    assert len(fake.calls) == 4  # 2 Chunks × j1/j2
    rows = {r["sub_norm"]: r["status"] for r in await db.list_subblocks(TOPIC, "beta")}
    assert set(rows.values()) == {"variant"}  # beide Dubletten global gefaltet
