//! v5.0 — selftest harness for the SDK retrogression gate.
//!
//! A selftest run binds one smix-owned booted sim, drives the in-repo
//! SelftestFixture SwiftUI app through a curated capability matrix, and
//! dumps a machine-judgeable `result.json` (+ a human-readable
//! `report.md`) to `.smix/selftest/<RFC3339>/`. The `latest` symlink in
//! `.smix/selftest/` always points to the most recent run so the
//! cycle-close gate can `jq` it without grovelling for the timestamp.
//!
//! The harness itself only records `pass` / `fail` / `skip` per
//! capability + a [`Narration`] block (screen / target / why / action)
//! that flows into both `result.json` and `report.md`. The *scenario*
//! dispatcher (`super::scenario`) decides what each capability segment
//! actually does. This separation lets a later cycle extend the matrix
//! without touching the harness file.

pub mod verifier;

pub mod scenario;

pub mod multi;

use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::os::unix::fs as unix_fs;
use std::path::{Path, PathBuf};

use chrono::{DateTime, SecondsFormat, Utc};
use serde_json::{Value, json};

pub use verifier::{
    C1_COVER, C2_DEFERRED, PERMANENT_SKIP, VerifyError, VerifyReport, grep_pub_fn, verify_cover,
    verify_cover_or_panic,
};

/// Per-capability narration block — the "UI story" the scenario tells
/// while driving a segment. Carried alongside the [`CapabilityStatus`]
/// so both `result.json` and `report.md` make the run reproducible /
/// debuggable without re-reading the scenario source.
///
/// Fields are deliberately free-form strings (not enums) — selftest is a
/// human-oriented gate, not a wire protocol; flexibility in narration
/// content matters more than schema rigidity.
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Narration {
    /// Screen the segment is exercising at this moment ("Login form" /
    /// "Home dashboard" / "Search modal" / etc).
    pub screen: String,
    /// The specific UI surface the segment touches ("Email field
    /// (id=input-email)" / "Continue button" / etc).
    pub target: String,
    /// Why this capability exists at all — the load-bearing one-liner
    /// that explains the API's role + any non-obvious constraint.
    pub why: String,
    /// The literal Rust call shape the segment dispatches ("app.fill(
    /// &id(\"input-email\"), \"qa@example.com\")"). Helps reviewers
    /// cross-reference against the scenario source.
    pub action: String,
}

impl Narration {
    /// Builder helper — `Narration::new(screen, target, why, action)`.
    pub fn new(
        screen: impl Into<String>,
        target: impl Into<String>,
        why: impl Into<String>,
        action: impl Into<String>,
    ) -> Self {
        Self {
            screen: screen.into(),
            target: target.into(),
            why: why.into(),
            action: action.into(),
        }
    }
}

/// One capability's outcome in a single selftest run.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CapabilityStatus {
    /// Capability segment finished without raising — selftest carries
    /// `evidence` forward into `result.json` as a short string the gate
    /// or a human can scan ("EXTINF=4" / "12 nodes" / a screenshot path).
    Passed {
        narration: Narration,
        evidence: String,
    },
    /// Capability raised. `failure_kind` is the `FailureCode` variant
    /// name so the gate can diff old/new runs; `message` is the
    /// AI-readable summary; `evidence_path` points at a dump (usually
    /// the screenshot we shoot right before the assertion).
    Failed {
        narration: Narration,
        failure_kind: String,
        message: String,
        evidence_path: Option<String>,
    },
    /// Capability segment never ran — fixture missing (no E2E env, no
    /// app installed) or upstream segment failed. Distinct from
    /// `Failed` so the gate can choose whether a skipped capability
    /// counts as regression or just "needs the user to bring the
    /// fixture back up" (see plan-cold/v5.0.md R5).
    Skipped {
        narration: Narration,
        reason: String,
    },
}

impl CapabilityStatus {
    fn status_str(&self) -> &'static str {
        match self {
            CapabilityStatus::Passed { .. } => "passed",
            CapabilityStatus::Failed { .. } => "failed",
            CapabilityStatus::Skipped { .. } => "skipped",
        }
    }

    fn narration(&self) -> &Narration {
        match self {
            CapabilityStatus::Passed { narration, .. }
            | CapabilityStatus::Failed { narration, .. }
            | CapabilityStatus::Skipped { narration, .. } => narration,
        }
    }
}

/// Container for one selftest run. Lives for the duration of the
/// scenario dispatcher and writes `result.json` + `report.md` at the
/// end. Capability segments call `record_pass` / `record_fail` /
/// `record_skip` — never `?` — so a single segment's failure does not
/// short-circuit the rest.
pub struct Harness {
    /// `.smix/selftest/<RFC3339>/` — created on `new`.
    run_dir: PathBuf,
    /// Insertion-ordered (BTreeMap == alphabetical) capability map —
    /// SDK pub-fn surface (`tap` / `fill` / `find_one` / etc).
    capability_status: BTreeMap<String, CapabilityStatus>,
    /// v5.0 C2 — selector / Modifiers / ExpectationFailure spec drill
    /// segments. Same `CapabilityStatus` enum, but a separate bucket
    /// because these segments exercise smix-selector field shapes +
    /// smix-error fallback chain (not SDK pub fn surface). result.json
    /// emits them as `selector_spec_segments` so the gate can diff them
    /// independently of the capability matrix.
    selector_spec_status: BTreeMap<String, CapabilityStatus>,
    /// v5.2 c7 — maestro yaml parity sub-test bucket. Cover ratio
    /// (passed-count) is orthogonal to the SDK pub-fn classification
    /// in `capability_status`. Skipped sub-tests are explicit defers
    /// (plan-doc'd v5.3+ work), not capability gaps; gate.sh treats
    /// `Skipped` here as acceptable (only `Failed` triggers exit 16).
    maestro_parity_status: BTreeMap<String, CapabilityStatus>,
    /// v5.3 c4 — V2 fixture e2e sub-test bucket. selftest scenario drives
    /// each `seg_v2_<area>_basic` directly through SDK pub fn (no maestro
    /// yaml, no adapter) and records the outcome here. result.json emits
    /// as `v2_e2e` + `v2_e2e_cover_count`. gate.sh exit codes 18 (missing
    /// segment) / 19 (Failed) / 20 (cover_count below V2_COVER_MIN).
    v2_e2e_status: BTreeMap<String, CapabilityStatus>,
    /// `git rev-parse HEAD` of the smix repo at run time — best-effort,
    /// empty string when the helper fails (e.g. running outside the
    /// repo).
    smix_commit: String,
    /// Same idea for the insight repo (the fixture the selftest drives).
    /// `None` when the helper can't find the repo.
    insight_commit: Option<String>,
    udid: String,
    started_at: DateTime<Utc>,
    /// v5.1 c4 — Capsule 软胶囊对账 summary。`None` 表示这次 selftest 没
    /// 走 capsule recording(裸跑模式;`smix capsule up` 起 runner 会带
    /// `TEST_RUNNER_SMIX_RECORD_ENABLED=1`,scenario 在 run_c1_segments 前
    /// 后 `start/stop_capsule_recording_and_reconcile` 填这字段)。
    capsule_reconcile: Option<CapsuleReconcileSummary>,
}

/// `result.json` 顶层 `capsule_reconcile` 字段 — v5.1 c4 硬胶囊化后
/// scenario 自检 SDK 自发 act 是否被外来 user 干预污染。`unattributed_count > 0`
/// 即 gate 失败(exit 11),指外来焦点变化未被任何 SDK issued 命中。
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct CapsuleReconcileSummary {
    pub issued_count: usize,
    pub focus_change_count: usize,
    pub attributed_count: usize,
    pub unattributed_count: usize,
    /// v5.2 c1 — 全量 unattributed event 明细落 result.json,gate dig 用。
    /// 不影响 unattributed_count 计算,只是把 capsule reconcile 的"哪些事件未
    /// 对账"暴露给 gate 操作员(避免每次都改 scenario 加 print)。
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub unattributed_events: Vec<smix_runner_client::RecordedEvent>,
}

impl CapsuleReconcileSummary {
    pub fn from_reconciliation(r: &crate::CapsuleReconciliation) -> Self {
        Self {
            issued_count: r.issued_count,
            focus_change_count: r.focus_change_count,
            attributed_count: r.attributed_count,
            unattributed_count: r.unattributed_count,
            unattributed_events: r.unattributed_events.clone(),
        }
    }
}

impl Harness {
    /// Create the run directory and capture the start timestamp +
    /// commit shas. The run dir name is the RFC3339 timestamp so two
    /// runs in the same minute don't collide.
    pub fn new(parent_dir: &Path, udid: String) -> io::Result<Self> {
        let started_at = Utc::now();
        // Filesystem-friendly RFC3339 (no `:` in the time, which OS X
        // tolerates but tools like `tar` historically don't).
        let dir_name = started_at
            .to_rfc3339_opts(SecondsFormat::Secs, true)
            .replace(':', "-");
        let run_dir = parent_dir.join(dir_name);
        Self::new_with_run_dir(run_dir, udid, started_at)
    }

    /// v5.1 c6 — multi-sim 并发 selftest 用。multi orchestrator 想要
    /// `.smix/selftest-multi/<RFC3339>/<UDID>/result.json` 这种 N+1 平铺,
    /// 不希望让每个 sim 再嵌一层 timestamp(`<RFC3339>/<UDID>/<RFC3339>/`)。
    /// 显式路径构造子让调用方完全自决最终落地位置。
    pub fn new_with_run_dir(
        run_dir: PathBuf,
        udid: String,
        started_at: DateTime<Utc>,
    ) -> io::Result<Self> {
        fs::create_dir_all(&run_dir)?;
        fs::create_dir_all(run_dir.join("screenshots"))?;

        // `git rev-parse HEAD` 走到 `.git`,从 run_dir 任一祖先都行。
        let smix_commit = git_head_sha(&run_dir).unwrap_or_default();
        let insight_commit = git_head_sha(Path::new(&insight_repo_path()));

        Ok(Self {
            run_dir,
            capability_status: BTreeMap::new(),
            selector_spec_status: BTreeMap::new(),
            maestro_parity_status: BTreeMap::new(),
            v2_e2e_status: BTreeMap::new(),
            smix_commit,
            insight_commit,
            udid,
            started_at,
            capsule_reconcile: None,
        })
    }

    /// v5.1 c4 — selftest example 调用,把对账 summary 钉进 result.json
    /// 顶层 `capsule_reconcile` 字段。
    pub fn set_capsule_reconcile(&mut self, summary: CapsuleReconcileSummary) {
        self.capsule_reconcile = Some(summary);
    }

    /// Where to dump screenshots and any other per-segment artifacts.
    /// Capability segments pre-resolve a path here and stash it in the
    /// `evidence_path` field so the gate can `ls` everything.
    pub fn run_dir(&self) -> &Path {
        &self.run_dir
    }

    /// Mark `name` as passed. The latest call wins if a capability is
    /// recorded twice — segments aren't supposed to do that but if a
    /// retry slipped in, re-recording is the safer default.
    pub fn record_pass(&mut self, name: &str, narration: Narration, evidence: &str) {
        print_narration_block(name, "PASS", &narration, evidence);
        self.capability_status.insert(
            name.to_string(),
            CapabilityStatus::Passed {
                narration,
                evidence: evidence.to_string(),
            },
        );
    }

    pub fn record_fail(
        &mut self,
        name: &str,
        narration: Narration,
        failure_kind: &str,
        message: &str,
        evidence_path: Option<String>,
    ) {
        print_narration_block(
            name,
            "FAIL",
            &narration,
            &format!("{failure_kind} — {message}"),
        );
        self.capability_status.insert(
            name.to_string(),
            CapabilityStatus::Failed {
                narration,
                failure_kind: failure_kind.to_string(),
                message: message.to_string(),
                evidence_path,
            },
        );
    }

    pub fn record_skip(&mut self, name: &str, narration: Narration, reason: &str) {
        print_narration_block(name, "SKIP", &narration, reason);
        self.capability_status.insert(
            name.to_string(),
            CapabilityStatus::Skipped {
                narration,
                reason: reason.to_string(),
            },
        );
    }

    /// v5.0 C2 — record a selector / Modifiers / fallback-chain spec
    /// segment as passed. Distinct from `record_pass` so the gate can
    /// surface "SDK capability matrix" and "Selector spec drill" as two
    /// separately readable artefacts.
    pub fn record_selector_spec_pass(&mut self, name: &str, narration: Narration, evidence: &str) {
        print_narration_block(name, "PASS", &narration, evidence);
        self.selector_spec_status.insert(
            name.to_string(),
            CapabilityStatus::Passed {
                narration,
                evidence: evidence.to_string(),
            },
        );
    }

    pub fn record_selector_spec_fail(
        &mut self,
        name: &str,
        narration: Narration,
        failure_kind: &str,
        message: &str,
        evidence_path: Option<String>,
    ) {
        print_narration_block(
            name,
            "FAIL",
            &narration,
            &format!("{failure_kind} — {message}"),
        );
        self.selector_spec_status.insert(
            name.to_string(),
            CapabilityStatus::Failed {
                narration,
                failure_kind: failure_kind.to_string(),
                message: message.to_string(),
                evidence_path,
            },
        );
    }

    /// v5.2 c7 — record a maestro yaml parity sub-test as passed.
    /// Distinct bucket from `capability_status` (SDK pub-fn cover) and
    /// `selector_spec_status` (selector field shapes). result.json
    /// emits as `maestro_parity` + `maestro_parity_cover_count`.
    pub fn record_maestro_parity_pass(&mut self, name: &str, narration: Narration, evidence: &str) {
        print_narration_block(name, "PASS", &narration, evidence);
        self.maestro_parity_status.insert(
            name.to_string(),
            CapabilityStatus::Passed {
                narration,
                evidence: evidence.to_string(),
            },
        );
    }

    /// v5.2 c7 — record a maestro yaml parity sub-test as failed.
    /// Triggers selftest-gate.sh exit 16.
    pub fn record_maestro_parity_fail(
        &mut self,
        name: &str,
        narration: Narration,
        failure_kind: &str,
        message: &str,
        evidence_path: Option<String>,
    ) {
        print_narration_block(
            name,
            "FAIL",
            &narration,
            &format!("{failure_kind} — {message}"),
        );
        self.maestro_parity_status.insert(
            name.to_string(),
            CapabilityStatus::Failed {
                narration,
                failure_kind: failure_kind.to_string(),
                message: message.to_string(),
                evidence_path,
            },
        );
    }

    /// v5.2 c7 — record a maestro yaml parity sub-test as skipped.
    /// Skipped sub-tests are explicit defers (plan-doc'd v5.3+ work);
    /// gate.sh treats Skipped as acceptable (does not contribute to
    /// `maestro_parity_cover_count`, but does not fail either).
    pub fn record_maestro_parity_skip(&mut self, name: &str, narration: Narration, reason: &str) {
        print_narration_block(name, "SKIP", &narration, reason);
        self.maestro_parity_status.insert(
            name.to_string(),
            CapabilityStatus::Skipped {
                narration,
                reason: reason.to_string(),
            },
        );
    }

    /// v5.2 c7 — borrow current maestro parity status map for scenario
    /// completion narration / unit tests.
    pub fn maestro_parity_status(&self) -> &BTreeMap<String, CapabilityStatus> {
        &self.maestro_parity_status
    }

    /// v5.3 c4 — record a V2 fixture e2e sub-test as passed.
    pub fn record_v2_e2e_pass(&mut self, name: &str, narration: Narration, evidence: &str) {
        print_narration_block(name, "PASS", &narration, evidence);
        self.v2_e2e_status.insert(
            name.to_string(),
            CapabilityStatus::Passed {
                narration,
                evidence: evidence.to_string(),
            },
        );
    }

    /// v5.3 c4 — record a V2 fixture e2e sub-test as failed. Triggers
    /// `selftest-gate.sh` exit 19.
    pub fn record_v2_e2e_fail(
        &mut self,
        name: &str,
        narration: Narration,
        failure_kind: &str,
        message: &str,
        evidence_path: Option<String>,
    ) {
        print_narration_block(
            name,
            "FAIL",
            &narration,
            &format!("{failure_kind} — {message}"),
        );
        self.v2_e2e_status.insert(
            name.to_string(),
            CapabilityStatus::Failed {
                narration,
                failure_kind: failure_kind.to_string(),
                message: message.to_string(),
                evidence_path,
            },
        );
    }

    /// v5.3 c4 — record a V2 fixture e2e sub-test as skipped (explicit defer).
    pub fn record_v2_e2e_skip(&mut self, name: &str, narration: Narration, reason: &str) {
        print_narration_block(name, "SKIP", &narration, reason);
        self.v2_e2e_status.insert(
            name.to_string(),
            CapabilityStatus::Skipped {
                narration,
                reason: reason.to_string(),
            },
        );
    }

    /// v5.3 c4 — borrow V2 e2e status map.
    pub fn v2_e2e_status(&self) -> &BTreeMap<String, CapabilityStatus> {
        &self.v2_e2e_status
    }

    pub fn record_selector_spec_skip(&mut self, name: &str, narration: Narration, reason: &str) {
        print_narration_block(name, "SKIP", &narration, reason);
        self.selector_spec_status.insert(
            name.to_string(),
            CapabilityStatus::Skipped {
                narration,
                reason: reason.to_string(),
            },
        );
    }

    /// `result.json` schema (see `docs/v5.md` §2):
    /// `{ timestamp, smix_commit, insight_commit, udid, capabilities[], coverage_pct, sdk_pub_fn_count, covered_count }`.
    ///
    /// Each entry in `capabilities` carries the four narration fields
    /// (`screen` / `target` / `why` / `action`) alongside the legacy
    /// status / evidence / failure_kind fields so the gate or a human
    /// can reconstruct the UI story from a single artifact.
    pub fn write_result_json(&self, verify_report: &VerifyReport) -> io::Result<PathBuf> {
        let path = self.run_dir.join("result.json");
        let capabilities: Vec<Value> = self
            .capability_status
            .iter()
            .map(|(name, status)| capability_to_json(name, status))
            .collect();
        let covered_count = self
            .capability_status
            .values()
            .filter(|s| matches!(s, CapabilityStatus::Passed { .. }))
            .count();
        let selector_spec_segments: Vec<Value> = self
            .selector_spec_status
            .iter()
            .map(|(name, status)| capability_to_json(name, status))
            .collect();
        // v5.2 c7 — maestro yaml parity sub-test bucket.
        let maestro_parity: Vec<Value> = self
            .maestro_parity_status
            .iter()
            .map(|(name, status)| capability_to_json(name, status))
            .collect();
        let maestro_parity_cover_count = self
            .maestro_parity_status
            .values()
            .filter(|s| matches!(s, CapabilityStatus::Passed { .. }))
            .count();
        // v5.3 c4 — V2 fixture e2e sub-test bucket.
        let v2_e2e: Vec<Value> = self
            .v2_e2e_status
            .iter()
            .map(|(name, status)| capability_to_json(name, status))
            .collect();
        let v2_e2e_cover_count = self
            .v2_e2e_status
            .values()
            .filter(|s| matches!(s, CapabilityStatus::Passed { .. }))
            .count();
        let json = json!({
            "timestamp": self.started_at.to_rfc3339_opts(SecondsFormat::Secs, true),
            "smix_commit": self.smix_commit,
            "insight_commit": self.insight_commit,
            "udid": self.udid,
            "capabilities": capabilities,
            "selector_spec_segments": selector_spec_segments,
            "maestro_parity": maestro_parity,
            "maestro_parity_cover_count": maestro_parity_cover_count,
            "v2_e2e": v2_e2e,
            "v2_e2e_cover_count": v2_e2e_cover_count,
            "capsule_reconcile": self.capsule_reconcile,
            "coverage_pct": verify_report.coverage_pct,
            "sdk_pub_fn_count": verify_report.sdk_pub_fn_count,
            "c1_cover_count": verify_report.c1_cover_count,
            "c2_deferred_count": verify_report.c2_deferred_count,
            "permanent_skip_count": verify_report.permanent_skip_count,
            "covered_count": covered_count,
        });
        fs::write(&path, serde_json::to_string_pretty(&json).unwrap())?;
        Ok(path)
    }

    /// Human-readable capability matrix — sister artefact to
    /// `result.json`, used in cycle capstones. Each capability gets a
    /// `### <name>` subsection with the full narration block so a
    /// reviewer can read the run top-to-bottom as a story.
    pub fn write_report_md(&self, verify_report: &VerifyReport) -> io::Result<PathBuf> {
        let path = self.run_dir.join("report.md");
        let mut md = String::new();
        md.push_str("# selftest report\n\n");
        md.push_str(&format!(
            "- timestamp: `{}`\n",
            self.started_at.to_rfc3339_opts(SecondsFormat::Secs, true)
        ));
        md.push_str(&format!("- udid: `{}`\n", self.udid));
        md.push_str(&format!("- smix_commit: `{}`\n", self.smix_commit));
        if let Some(ic) = &self.insight_commit {
            md.push_str(&format!("- insight_commit: `{ic}`\n"));
        }
        md.push_str(&format!(
            "- coverage: **{:.1}%** ({}/{} sdk pub fn covered by C1 selftest, C2 deferred {}, permanent skip {})\n\n",
            verify_report.coverage_pct,
            verify_report.c1_cover_count,
            verify_report.sdk_pub_fn_count,
            verify_report.c2_deferred_count,
            verify_report.permanent_skip_count,
        ));

        md.push_str("## capability matrix\n\n");
        render_status_matrix(&mut md, &self.capability_status);
        if !self.selector_spec_status.is_empty() {
            md.push_str("## selector / failure spec drill\n\n");
            render_status_matrix(&mut md, &self.selector_spec_status);
        }
        fs::write(&path, md)?;
        Ok(path)
    }

    /// Atomically swap `.smix/selftest/latest` to point at this run.
    /// Uses a tmp-symlink + rename to avoid leaving a half-broken
    /// symlink if the process dies mid-call.
    pub fn update_latest_symlink(&self) -> io::Result<()> {
        let parent = self.run_dir.parent().expect("run_dir has a parent");
        let latest = parent.join("latest");
        let tmp = parent.join("latest.tmp");
        let _ = fs::remove_file(&tmp);
        // Symlink to the dir name only (relative), not the absolute
        // path — keeps `.smix/selftest/` self-contained when copied.
        let target = self.run_dir.file_name().expect("run_dir has a name");
        unix_fs::symlink(target, &tmp)?;
        // `fs::rename` is atomic on POSIX when both paths are on the
        // same filesystem (always true for two paths in the same dir).
        fs::rename(tmp, latest)?;
        Ok(())
    }

    /// Compute the process exit code from the recorded statuses:
    /// `0` iff every recorded capability is `Passed`, otherwise non-zero.
    /// Skipped segments do count as failures — the gate's whole point
    /// is "every capability either ran clean or surfaced the reason",
    /// so a skip the operator hasn't acknowledged is still a regression.
    ///
    /// Also prints the final narration-roll-up totals to stderr so the
    /// gate operator sees `passed / failed / skipped` at a glance.
    pub fn finish(&self) -> i32 {
        let (cap_passed, cap_failed, cap_skipped) = tally(&self.capability_status);
        let (spec_passed, spec_failed, spec_skipped) = tally(&self.selector_spec_status);
        eprintln!(
            "selftest narration roll-up: capability {}P/{}F/{}S (total {}) + selector_spec {}P/{}F/{}S (total {})",
            cap_passed,
            cap_failed,
            cap_skipped,
            self.capability_status.len(),
            spec_passed,
            spec_failed,
            spec_skipped,
            self.selector_spec_status.len(),
        );
        if cap_failed + cap_skipped + spec_failed + spec_skipped > 0 {
            2
        } else {
            0
        }
    }

    /// Mostly for tests + the example's end-of-run summary print.
    pub fn capability_status(&self) -> &BTreeMap<String, CapabilityStatus> {
        &self.capability_status
    }

    /// v5.0 C2 — selector / Modifiers / fallback-chain spec drill statuses.
    pub fn selector_spec_status(&self) -> &BTreeMap<String, CapabilityStatus> {
        &self.selector_spec_status
    }
}

fn tally(map: &BTreeMap<String, CapabilityStatus>) -> (u32, u32, u32) {
    let mut passed = 0u32;
    let mut failed = 0u32;
    let mut skipped = 0u32;
    for s in map.values() {
        match s {
            CapabilityStatus::Passed { .. } => passed += 1,
            CapabilityStatus::Failed { .. } => failed += 1,
            CapabilityStatus::Skipped { .. } => skipped += 1,
        }
    }
    (passed, failed, skipped)
}

fn render_status_matrix(md: &mut String, statuses: &BTreeMap<String, CapabilityStatus>) {
    for (name, status) in statuses {
        let status_label = match status {
            CapabilityStatus::Passed { .. } => "PASS",
            CapabilityStatus::Failed { .. } => "FAIL",
            CapabilityStatus::Skipped { .. } => "SKIP",
        };
        let evidence_line = match status {
            CapabilityStatus::Passed { evidence, .. } => evidence.clone(),
            CapabilityStatus::Failed {
                failure_kind,
                message,
                evidence_path,
                ..
            } => {
                let mut line = format!("{failure_kind} — {message}");
                if let Some(p) = evidence_path {
                    line.push_str(&format!(" (evidence: `{p}`)"));
                }
                line
            }
            CapabilityStatus::Skipped { reason, .. } => reason.clone(),
        };
        let n = status.narration();
        md.push_str(&format!("### `{name}` [{status_label}]\n"));
        md.push_str(&format!("- **screen:** {}\n", n.screen));
        md.push_str(&format!("- **target:** {}\n", n.target));
        md.push_str(&format!("- **why:** {}\n", n.why));
        md.push_str(&format!("- **action:** `{}`\n", n.action));
        md.push_str(&format!("- **evidence:** {evidence_line}\n\n"));
    }
}

fn print_narration_block(name: &str, status: &str, narration: &Narration, tail: &str) {
    eprintln!("--- selftest segment: {name} [{status}] ---");
    eprintln!("  screen: {}", narration.screen);
    eprintln!("  target: {}", narration.target);
    eprintln!("  why:    {}", narration.why);
    eprintln!("  action: {}", narration.action);
    eprintln!("  {}: {tail}", status.to_lowercase());
}

fn capability_to_json(name: &str, status: &CapabilityStatus) -> Value {
    let n = status.narration();
    match status {
        CapabilityStatus::Passed { evidence, .. } => json!({
            "name": name,
            "status": status.status_str(),
            "screen": n.screen,
            "target": n.target,
            "why": n.why,
            "action": n.action,
            "evidence": evidence,
            "failure_kind": Value::Null,
        }),
        CapabilityStatus::Failed {
            failure_kind,
            message,
            evidence_path,
            ..
        } => json!({
            "name": name,
            "status": status.status_str(),
            "screen": n.screen,
            "target": n.target,
            "why": n.why,
            "action": n.action,
            "failure_kind": failure_kind,
            "message": message,
            "evidence_path": evidence_path,
        }),
        CapabilityStatus::Skipped { reason, .. } => json!({
            "name": name,
            "status": status.status_str(),
            "screen": n.screen,
            "target": n.target,
            "why": n.why,
            "action": n.action,
            "reason": reason,
        }),
    }
}

fn git_head_sha(repo: &Path) -> Option<String> {
    let out = std::process::Command::new("git")
        .args(["rev-parse", "HEAD"])
        .current_dir(repo)
        .output()
        .ok()?;
    if !out.status.success() {
        return None;
    }
    let sha = String::from_utf8(out.stdout).ok()?.trim().to_string();
    if sha.is_empty() { None } else { Some(sha) }
}

fn insight_repo_path() -> String {
    std::env::var("INSIGHT_REPO_PATH").unwrap_or_else(|_| {
        let home = std::env::var("HOME").unwrap_or_default();
        format!("{home}/workspace/qualcomm/insight")
    })
}
