"""Fake-E2E: kompletter Scan-Lauf (Chunker → Board → .planer/) in Sekunden, ohne LLM.
Die Fake-Antworten durchlaufen die ECHTEN Gates — getestet wird der volle Pfad
inkl. Nachfass, Retry, Dead-Letter und Infra-Pause."""
import asyncio
import shutil
from pathlib import Path

import fake_agents
import kanban
import scan_board

MINI = Path(__file__).parent / "fixtures" / "mini_repo"


def _repo(tmp_path: Path) -> Path:
    ziel = tmp_path / "repo"
    shutil.copytree(MINI, ziel)
    return ziel


async def _scan(repo: Path, monkeypatch) -> dict:
    monkeypatch.setattr(kanban, "RETRY_BACKOFF", 0.05)
    return await asyncio.wait_for(
        scan_board.run_scan(repo, "mini", stand="deadbeef"), timeout=30)


async def test_e2e_voller_scan(fake_welt, testdb, tmp_path, monkeypatch):
    fake_agents.standard_antworten(fake_welt)
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "ok", ergebnis
    planer = repo / ".planer"
    assert (planer / "einheiten" / "rechner.py.md").is_file()
    assert (planer / "einheiten" / "anzeige.py.md").is_file()
    assert (planer / "kern.md").read_text().startswith("# Kern: mini")
    assert (planer / "architektur.md").is_file()
    assert (planer / "flows.md").is_file()
    assert (planer / "pruefung.md").is_file()
    assert list((planer / "features").glob("*.md"))
    einheiten = (planer / "einheiten" / "rechner.py.md").read_text()
    assert "stand: deadbeef" in einheiten
    assert "## rechner.py::addiere" in einheiten and "## rechner.py::Speicher" in einheiten


async def test_e2e_nachfass_pfad(fake_welt, testdb, tmp_path, monkeypatch):
    """Erst-Scan lässt `skaliere` aus → Abdeckungs-Gate schickt den Chunk in den Nachfass."""
    fake_agents.standard_antworten(fake_welt, lasse_aus={"skaliere"})
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "ok", ergebnis
    assert any("-nachfass-" in k for k in fake_welt.calls)
    assert "## rechner.py::skaliere" in (repo / ".planer" / "einheiten" / "rechner.py.md").read_text()


async def test_e2e_stoerung_retry(fake_welt, testdb, tmp_path, monkeypatch):
    """Einmal Garbage → Gates lehnen ab → Engine-Backoff → zweiter Versuch gelingt."""
    fake_agents.standard_antworten(fake_welt)
    fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "garbage", "mal": 1})
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "ok", ergebnis


async def test_e2e_dauerfehler_dead(fake_welt, testdb, tmp_path, monkeypatch):
    """Dauerhafter Fehler auf einem Chunk → Karte landet im Dead-Letter, Lauf endet."""
    fake_agents.standard_antworten(fake_welt)
    fake_welt.stoerungen.append({"muster": r"-schneiden-chunk-0000", "modus": "fehler", "mal": 99})
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "fehler"
    assert ergebnis["dead"] == ["chunk-0000"]


async def test_e2e_infra_pause(fake_welt, testdb, tmp_path, monkeypatch):
    """AgentInfraError → Lauf pausiert sofort (kein Dead-Letter, kein Raten)."""
    fake_agents.standard_antworten(fake_welt)
    fake_welt.stoerungen.append({"muster": r"-schneiden-", "modus": "infra", "mal": 1})
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "pausiert"
    assert "fake-infra" in ergebnis["infra_error"]


async def test_e2e_fortsetzen_eine_sichten_runde(fake_welt, testdb, tmp_path, monkeypatch):
    """Fortsetzung mit Mix aus toten und fertigen Chunks: die Sammeln-Barriere darf erst
    öffnen, wenn der Producer ALLE Karten requeued hat — genau eine Sichten-Runde, und
    die Sichten sehen die volle Übersicht (Abnahme-Befund: Teil-Übersicht durch frühes
    Drainen)."""
    fake_agents.standard_antworten(fake_welt)
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "ok"
    # Zustand eines abgebrochenen Laufs nachstellen: ein Chunk tot, Rest fertig
    karten = await testdb.kanban_cards("mini", scan_board.BOARD, kind="chunk")
    tot = karten[0]["card_id"]
    await testdb.kanban_upsert_card("mini", scan_board.BOARD, tot, "chunk", "dead", None)
    fake_welt.calls.clear()
    ergebnis2 = await asyncio.wait_for(
        scan_board.run_scan(repo, "mini", stand="deadbeef", fortsetzen=True), timeout=30)
    assert ergebnis2["status"] == "ok", ergebnis2
    sichten_calls = [k for k in fake_welt.calls if "-sichten-" in k]
    assert len(sichten_calls) == 3  # genau EINE Runde, kein Doppel-Drain
    features = (repo / ".planer" / "features" / "fake-bereich.md").read_text()
    assert "rechner.py::addiere" in features and "anzeige.py::zeige" in features  # volle Übersicht


async def test_e2e_events_geschrieben(fake_welt, testdb, tmp_path, monkeypatch):
    """Telemetrie: Agent-Events mit run_id landen in der DB."""
    import agents
    import database
    monkeypatch.setattr(agents, "on_event", database.add_event)
    fake_agents.standard_antworten(fake_welt)
    repo = _repo(tmp_path)
    ergebnis = await _scan(repo, monkeypatch)
    assert ergebnis["status"] == "ok"
    db = await testdb.get_db()
    cursor = await db.execute(
        "SELECT count(*) FROM events WHERE topic='mini' AND kind='agent' AND run_id LIKE 'scan-%'")
    assert (await cursor.fetchone())[0] >= 5  # 2 Chunks + 3 Sichten
