"""The level ladder — five rungs, no capping (phase lvl5, plan-phase-lvl5-lint-rung.md).

A single integer **level** summarising how far up the quality ladder a recipe run climbed:
  L0 — install failed / app never became healthy.
  L1 — Installs: deploys + passes health/readiness.
  L2 — Upgrades: previous published version → PR version, stays healthy, data intact.
  L3 — Backup/restore: seeded data survives backup → wipe → restore.
  L4 — Functional: recipe-specific functional tests pass.
  L5 — Lint: `abra recipe lint` passes against the exact ref under test.

Semantics (operator-decided 2026-06-11, recorded in DECISIONS.md — replaces the Phase-3
"N/A caps" rule):

    level = max i such that rung_i == "pass" and every rung j < i is "pass" or "skip"; 0 if none.

A rung has one of FOUR statuses:
  "pass"  — exercised and passed.
  "fail"  — exercised and failed. Blocks: no rung above it can count.
  "skip"  — INTENTIONAL skip: the rung genuinely does not apply to this recipe, from a
            declared or structural fact (not backup-capable; only one published version;
            declared in recipe_meta.EXPECTED_NA). Does NOT stop the climb.
  "unver" — UNINTENTIONAL not-verified: the rung SHOULD have run but didn't (infra error,
            missing tool, harness exception, prior-stage abort, timeout). Blocks exactly
            like a fail — the level never rises above a rung that wasn't actually checked.

The per-rung table (results.json `rungs`, card, dashboard) is the SOLE carrier of "why isn't
this level higher" — there is no cap_reason. The classification of every N/A source into
skip-vs-unver lives in derive_rungs (results.py) and is tabulated in DECISIONS.md; anything
unclassifiable defaults to "unver" (conservative: never claim what wasn't checked).

Integration (SSO/OIDC + cross-app) and recipe-local (the recipe repo's own tests/) remain
OPTIONAL capabilities — not rungs, never counted (SSO is still enforced for the run VERDICT
via the deps/SSO checks in run_recipe_ci.py).

This module is PURE (no I/O) so it is cheaply unit-testable and the Adversary can re-run the
unit test cold (`cc-ci-run -m pytest tests/unit/test_level.py -q`).
"""

from __future__ import annotations

# The climbable rungs in ascending order. install (L1) is the foundation; L0 means install
# itself did not pass. These five are the ESSENTIAL rungs — integration/recipe-local are
# optional and deliberately NOT in this tuple.
RUNGS = ("install", "upgrade", "backup_restore", "functional", "lint")

# Human-readable label per rung level, for the summary card / docs.
RUNG_LABEL = {
    1: "install (deploy + health)",
    2: "upgrade (prev published → PR)",
    3: "backup/restore (data integrity)",
    4: "functional (recipe-specific tests)",
    5: "lint (abra recipe lint)",
}

VALID = {"pass", "fail", "skip", "unver"}


def compute_level(rungs: dict[str, str]) -> int:
    """Map a rung-status dict → level 0..5.

    `rungs` must contain a status in VALID for every name in RUNGS. The level is the highest
    i such that rungs[i] == "pass" and every rung below i is "pass" or "skip" (an intentional
    skip does not stop the climb). A "fail" or "unver" rung blocks: rungs above it cannot
    count, however green. 0 when no rung qualifies.
    """
    for name in RUNGS:
        st = rungs.get(name)
        if st not in VALID:
            raise ValueError(
                f"rung {name!r} has invalid status {st!r} (expect one of {sorted(VALID)})"
            )
    level = 0
    for idx, name in enumerate(RUNGS, start=1):
        st = rungs[name]
        if st == "pass":
            level = idx
        elif st == "skip":
            continue
        else:  # fail / unver — nothing above this rung can count
            break
    return level


def backup_restore_status(backup: str | None, restore: str | None, backup_capable: bool) -> str:
    """Collapse the backup + restore tier results into the single L3 rung status.

    Not backup-capable (a declared/structural fact: no backupbot labels, or
    recipe_meta.BACKUP_CAPABLE=False) → "skip" — the rung genuinely does not apply.
    Otherwise both tiers must pass for the rung to pass; a fail in either tier fails it; any
    other shape (tier skipped or never ran while backup-capable — e.g. a prior-stage abort)
    is "unver": the rung should have been verified and wasn't.
    """
    if not backup_capable:
        return "skip"
    vals = {backup, restore}
    if "fail" in vals:
        return "fail"
    if backup == "pass" and restore == "pass":
        return "pass"
    return "unver"


def tier_to_rung(status: str | None) -> str:
    """Map a single tier result ('pass'|'fail'|'skip'|None) to a rung status, with NO
    intentionality information: a tier that did not produce a pass/fail is "unver" (it should
    have run and wasn't verified). The caller (derive_rungs) upgrades "unver" to "skip" where
    a declared/structural fact makes the skip intentional — never the other way around."""
    if status == "pass":
        return "pass"
    if status == "fail":
        return "fail"
    return "unver"
