"""Phase 3 U4 — dashboard YunoHost-style grid + per-recipe history (pure-render + helpers).

The dashboard reads a Drone admin token at import; point DRONE_TOKEN_FILE at a temp file so the
module imports without the real secret. All tests here are pure (no network): they exercise the
rendering + results.json projection, asserting the grid/history mirror the artifact and never present
a run greener than its data (R5 / cardinal guardrail)."""

from __future__ import annotations

import json
import os
import sys
import tempfile

with tempfile.NamedTemporaryFile("w", delete=False, suffix=".tok") as _tok:
    _tok.write("test-token")
os.environ["DRONE_TOKEN_FILE"] = _tok.name
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "dashboard"))

import dashboard  # noqa: E402


def _row(**kw):
    base = {
        "recipe": "custom-html",
        "status": "success",
        "number": 4,
        "ref": "db9a9502",
        "version": "db9a95024e9d",
        "level": 4,
        "has_screenshot": True,
        "flags": {"clean_teardown": True, "no_secret_leak": True},
        "finished": 0,
        "url": "https://drone.x/cc-ci/4",
    }
    base.update(kw)
    return base


def test_level_color_ramp_and_fallback():
    assert dashboard.level_color(0) == "#e5534b"
    assert dashboard.level_color(5) == "#3fb950"  # full 5-rung climb (phase lvl5)
    assert dashboard.level_color(4) == "#a0b93f"
    assert dashboard.level_color(99) == "#8b949e"
    assert dashboard.level_color(None) == "#8b949e"


def test_overview_grid_mirrors_results():
    out = dashboard.render_overview([_row()])
    assert "custom-html" in out
    assert "level 4" in out  # the corner level pill
    assert dashboard.level_color(4) in out  # coloured by level
    assert "db9a95024e9d" in out  # version from results.json
    assert "/runs/4/screenshot.png" in out  # thumbnail
    assert "/runs/4/summary.png" in out  # links to full card
    assert "/recipe/custom-html" in out  # history link
    assert "✔ teardown" in out and "✔ no-leak" in out


def test_overview_never_greener_than_data():
    # A failed run at level 0 must show level 0 + the failure pill — never a green/high level.
    out = dashboard.render_overview(
        [_row(status="failure", level=0, has_screenshot=False, flags={})]
    )
    assert "level 0" in out
    assert dashboard.level_color(0) in out  # red
    assert dashboard._COLORS["failure"] in out
    assert "level 4" not in out and "level 5" not in out
    assert "no screenshot" in out  # placeholder, no broken image


def test_level_pill_unknown_when_no_results():
    assert "level —" in dashboard._level_pill(None)
    assert "#8b949e" in dashboard._level_pill(None)


def test_history_table_lists_runs():
    out = dashboard.render_history("custom-html", [_row(number=4), _row(number=3, level=2)])
    assert "custom-html — run history" in out
    assert "#4" in out and "#3" in out
    assert "L4" in out and "L2" in out
    assert "← all recipes" in out
    assert "/runs/4/summary.png" in out  # per-run card link


def test_history_empty():
    out = dashboard.render_history("hedgedoc", [])
    assert "no runs for this recipe yet" in out


def test_build_row_projects_results(monkeypatch):
    monkeypatch.setattr(
        dashboard,
        "_results_for",
        lambda n: {
            "version": "1.2.3",
            "level": 2,
            "screenshot": "screenshot.png",
            "flags": {"clean_teardown": True},
        },
    )
    b = {
        "number": 7,
        "status": "success",
        "event": "custom",
        "params": {"RECIPE": "n8n", "REF": "abcdef1234567890"},
        "finished": 10,
    }
    r = dashboard._build_row(b)
    assert r["recipe"] == "n8n" and r["number"] == 7
    assert r["level"] == 2 and r["version"] == "1.2.3"
    assert r["has_screenshot"] is True
    assert r["url"].endswith("/cc-ci/7")


def test_build_row_old_schema1_artifact_renders(monkeypatch):
    # History compatibility (phase lvl5): pre-lvl5 results.json still carries cap fields and a
    # 4-rung ladder — it must project + render without KeyError, level shown VERBATIM (no
    # retroactive relabeling), and the old cap text simply isn't resurfaced anywhere.
    monkeypatch.setattr(
        dashboard,
        "_results_for",
        lambda n: {
            "schema": 1,
            "version": "0.9.1",
            "level": 2,
            "level_cap_reason": "L3 backup/restore (data integrity) N/A",
            "level_cap_rung": "backup_restore",
            "screenshot": "screenshot.png",
            "flags": {"clean_teardown": True, "no_secret_leak": True},
        },
    )
    b = {
        "number": 11,
        "status": "success",
        "event": "custom",
        "params": {"RECIPE": "legacy", "REF": "abc123"},
        "finished": 5,
    }
    r = dashboard._build_row(b)
    out = dashboard.render_overview([r])
    assert "level 2" in out and dashboard.level_color(2) in out
    assert "N/A" not in out and "capped" not in out  # cap language gone from the surface
    hist = dashboard.render_history("legacy", [r])
    assert "L2" in hist


def test_build_row_degrades_without_results(monkeypatch):
    # No results.json (e.g. an old run): grid still renders from Drone fields, level absent.
    monkeypatch.setattr(dashboard, "_results_for", lambda n: {})
    b = {
        "number": 9,
        "status": "running",
        "event": "custom",
        "params": {"RECIPE": "ghost", "REF": "deadbeefcafe1234567890"},
        "finished": 0,
    }
    r = dashboard._build_row(b)
    assert r["level"] is None and r["has_screenshot"] is False
    assert r["version"] == "deadbeefcafe"  # ref[:12] fallback
    # render must not crash or claim a level
    assert "level —" in dashboard.render_overview([r])


def test_level_badge_shows_level_coloured(monkeypatch):
    svg = dashboard.render_level_badge("custom-html", 4)
    assert "custom-html" in svg and "level 4" in svg
    assert dashboard.level_color(4) in svg  # coloured by level
    assert svg.startswith("<svg") and "image" not in svg  # plain SVG
    # A higher displayed level than earned would be inflation — badge shows exactly the given level.
    assert "level 5" not in svg and "level 6" not in svg


def _write_run(base, run_id, recipe, finished, **kw):
    d = os.path.join(base, run_id)
    os.makedirs(d, exist_ok=True)
    doc = {
        "recipe": recipe,
        "finished": finished,
        "run_id": run_id,
        "ref": kw.get("ref", "deadbeefcafe"),
        "version": kw.get("version"),
        "level": kw.get("level", 5),
        "screenshot": kw.get("screenshot", "screenshot.png"),
        "results": kw.get("results", {"install": "pass"}),
        "flags": kw.get("flags", {}),
    }
    with open(os.path.join(d, "results.json"), "w") as fh:
        json.dump(doc, fh)


def test_history_sourced_from_local_artifacts(tmp_path, monkeypatch):
    """phase dash: history_for sources the FULL per-recipe run list from CCCI_RUNS_DIR (not the
    capped Drone slice), newest-first by `finished` even with mixed numeric+named run ids, capped,
    and skips malformed/empty/no-recipe dirs without raising."""
    base = str(tmp_path)
    monkeypatch.setattr(dashboard, "CCCI_RUNS_DIR", base)
    monkeypatch.setattr(dashboard, "HISTORY_CAP", 3)
    dashboard._LOCAL.update(ts=0.0, by_recipe={})  # bypass scan cache
    # mixed numeric + named ids; out-of-order on disk; the timestamp MUST decide order, not the id
    _write_run(base, "753", "bsky", 1781663348, results={"install": "pass"})
    _write_run(base, "427", "bsky", 1781178768, results={"install": "pass"})
    _write_run(
        base, "m2r-bsky", "bsky", 1781121610, level=0, results={"install": "pass", "backup": "fail"}
    )
    _write_run(
        base, "423", "bsky", 1781178063, results={"install": "pass"}
    )  # 423<427 numerically but OLDER
    _write_run(base, "9", "other", 1781000000)  # different recipe, must not leak in
    # graceful-skip cases (the host's in-flight/failed-early dirs)
    os.makedirs(os.path.join(base, "EMPTY"), exist_ok=True)  # in-flight dir, no results.json
    os.makedirs(os.path.join(base, "MALFORMED"), exist_ok=True)
    with open(os.path.join(base, "MALFORMED", "results.json"), "w") as fh:
        fh.write("{ not json")
    os.makedirs(os.path.join(base, "NORECIPE"), exist_ok=True)
    with open(os.path.join(base, "NORECIPE", "results.json"), "w") as fh:
        json.dump({"finished": 1.0}, fh)

    rows = dashboard.history_for("bsky")
    # 4 bsky runs but HISTORY_CAP=3 → 3 newest, in finished-desc order (753,427,423 — NOT by id)
    assert [r["number"] for r in rows] == ["753", "427", "423"]
    assert [r["finished"] for r in rows] == [1781663348, 1781178768, 1781178063]
    # capped: the oldest (m2r-bsky) is dropped, newest kept
    assert "m2r-bsky" not in [r["number"] for r in rows]
    # status derived from per-stage results map (no top-level status field)
    assert rows[0]["status"] == "success"
    # numeric id → Drone link; named id → local summary link
    assert rows[0]["url"].endswith("/753")
    dashboard._LOCAL.update(ts=0.0, by_recipe={})
    monkeypatch.setattr(dashboard, "HISTORY_CAP", 30)
    full = dashboard.history_for("bsky")
    assert [r["number"] for r in full] == ["753", "427", "423", "m2r-bsky"]
    assert full[-1]["url"] == "/runs/m2r-bsky/summary.html"  # named id → local summary
    # malformed/empty/no-recipe dirs never surface as recipes and never raise
    assert "other" not in dashboard._local_history() or True
    assert set(dashboard._local_history().keys()) == {"bsky", "other"}
    assert dashboard.history_for("nope") == []


def test_status_badge_fallback_when_no_level():
    # Recipe with no results.json level → status badge, not a fabricated level.
    svg = dashboard.render_badge("ghost", "failure")
    assert "failure" in svg and "level" not in svg
    assert dashboard._COLORS["failure"] in svg


def test_results_for_traversal_guarded():
    with tempfile.TemporaryDirectory() as d:
        os.makedirs(os.path.join(d, "5"))
        with open(os.path.join(d, "5", "results.json"), "w") as f:
            json.dump({"level": 3}, f)
        orig = dashboard.CCCI_RUNS_DIR
        dashboard.CCCI_RUNS_DIR = d
        try:
            assert dashboard._results_for("5") == {"level": 3}
            assert dashboard._results_for("../../etc") == {}  # traversal rejected
            assert dashboard._results_for("nonexist") == {}  # missing → {}
            assert dashboard._results_for("") == {}
        finally:
            dashboard.CCCI_RUNS_DIR = orig
