//! Local-first workbench (WOR-1429).
//!
//! Official Inspector and MCPJam win the first-run visual experience. mcptest
//! has richer governance and evidence primitives but a CLI-first UX. The
//! workbench closes that gap without a hosted account: it gathers saved
//! artifacts (a tools/list snapshot, a cassette, a run report, a security
//! report, an evidence pack) or a live capture into one self-contained HTML
//! page with capability, trace, results, security, and evidence panes, plus an
//! inspect-to-evidence workflow guide.
//!
//! Everything is local: no network, no CDN, no account, no telemetry. The page
//! opens from `file://`. Hosted dashboards, shared workbenches, org history, and
//! RBAC stay in `mcptest-enterprise`.

mod extract;
mod html;
mod model;

pub use html::render_html;
pub use model::{
    CapItem, EvidenceMeta, ResultRow, ResultsSummary, SecItem, TraceEntry, WorkbenchData,
};

use serde_json::Value;

/// Optional parsed artifacts to gather into a workbench.
#[derive(Debug, Default)]
pub struct WorkbenchInputs {
    /// A tools/list-style snapshot (tools/resources/prompts).
    pub snapshot: Option<Value>,
    /// A recorded cassette (the JSON-RPC trace).
    pub cassette: Option<Value>,
    /// A run report (the results).
    pub report: Option<Value>,
    /// A security report (the findings).
    pub security: Option<Value>,
    /// An evidence pack (the metadata).
    pub evidence: Option<Value>,
}

/// Gather the inputs into a [`WorkbenchData`].
///
/// `source_label` describes where the data came from; `live` marks a live
/// capture. Missing inputs simply leave their panes empty.
pub fn build_workbench(inputs: &WorkbenchInputs, source_label: &str, live: bool) -> WorkbenchData {
    let mut data = WorkbenchData {
        source_label: source_label.to_string(),
        live,
        ..Default::default()
    };
    if let Some(snapshot) = &inputs.snapshot {
        data.tools = extract::capabilities(snapshot, "tools");
        data.resources = extract::capabilities(snapshot, "resources");
        data.prompts = extract::capabilities(snapshot, "prompts");
    }
    if let Some(cassette) = &inputs.cassette {
        data.trace = extract::trace(cassette);
    }
    if let Some(report) = &inputs.report {
        data.results = extract::results(report);
    }
    if let Some(security) = &inputs.security {
        data.security = extract::security(security);
    }
    if let Some(pack) = &inputs.evidence {
        data.evidence = extract::evidence(pack);
    }
    data
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn sample_inputs() -> WorkbenchInputs {
        WorkbenchInputs {
            snapshot: Some(json!({
                "tools": [{"name": "get_weather", "description": "Look up weather"}],
                "resources": [{"name": "doc://readme"}],
                "prompts": []
            })),
            cassette: Some(json!({
                "interactions": [
                    {"request": {"method": "tools/list"}, "response": {"result": {"tools": []}}}
                ]
            })),
            report: Some(json!({
                "summary": {"total": 2, "passed": 1, "failed": 1, "skipped": 0},
                "tests": [
                    {"name": "lists tools", "status": "passed"},
                    {"name": "calls weather", "passed": false}
                ]
            })),
            security: Some(json!({
                "findings": [{"id": "SEC-001", "severity": "high", "title": "toxic flow"}]
            })),
            evidence: Some(json!({
                "schema_version": "mcptest.dev/evidence-pack/v1",
                "digest": "sha256:abc",
                "generated_at": "2026-06-22T00:00:00Z",
                "sections": {"coverage": {}, "posture": {}}
            })),
        }
    }

    #[test]
    fn gathers_every_pane_from_artifacts() {
        let data = build_workbench(&sample_inputs(), "artifacts", false);
        assert_eq!(data.tools.len(), 1);
        assert_eq!(data.resources.len(), 1);
        assert_eq!(data.trace.len(), 2, "request and response rows");
        let results = data.results.as_ref().unwrap();
        assert_eq!(results.failed, 1);
        assert_eq!(results.rows.len(), 2);
        assert_eq!(data.security.len(), 1);
        assert_eq!(data.evidence.as_ref().unwrap().sections.len(), 2);
        assert!(!data.is_empty());
    }

    #[test]
    fn renders_self_contained_html_with_no_network() {
        let data = build_workbench(&sample_inputs(), "artifacts", false);
        let html = render_html(&data);
        assert!(html.starts_with("<!DOCTYPE html>"));
        assert!(html.contains("get_weather"));
        assert!(html.contains("toxic flow"));
        assert!(html.contains("Inspect to evidence"));
        // No external resources: nothing fetched over the network.
        assert!(!html.contains("http://"));
        assert!(!html.contains("https://"));
        assert!(!html.contains("src=\""));
    }

    #[test]
    fn escapes_embedded_artifact_text() {
        let inputs = WorkbenchInputs {
            snapshot: Some(
                json!({"tools": [{"name": "x", "description": "<script>alert(1)</script>"}]}),
            ),
            ..Default::default()
        };
        let html = render_html(&build_workbench(&inputs, "x", false));
        assert!(!html.contains("<script>alert(1)"));
        assert!(html.contains("&lt;script&gt;"));
    }

    #[test]
    fn empty_inputs_yield_an_empty_workbench() {
        let data = build_workbench(&WorkbenchInputs::default(), "none", false);
        assert!(data.is_empty());
    }
}
