//! JSON reporter.

use std::io::Write;
use std::sync::Arc;

use serde::Serialize;
use serde_json::Value;

use crate::coverage::CoverageReport;
use crate::redaction::Redactor;
use crate::report::{Report, Reporter};

/// JSON reporter, pretty-printed for readability.
///
/// Accepts an optional [`Redactor`] via [`JsonReporter::with_redactor`].
/// When set, the report is redacted before serialization. Most callers
/// should prefer [`Report::redacted`] at the reporter dispatch site so
/// every format shares one redacted view; the builder exists for
/// callers that drive one reporter directly.
///
/// Coverage flows in sideways via [`JsonReporter::with_coverage`] so the
/// runner can attach a [`CoverageReport`] without growing a new field on
/// the canonical [`Report`]. The reporter serializes a top-level `coverage`
/// key when one is attached; otherwise the JSON shape is unchanged.
#[derive(Default)]
pub struct JsonReporter {
    redactor: Option<Arc<Redactor>>,
    coverage: Option<CoverageReport>,
}

impl JsonReporter {
    /// Attach a redactor so the serialized report is redacted first.
    pub fn with_redactor(mut self, redactor: Arc<Redactor>) -> Self {
        self.redactor = Some(redactor);
        self
    }

    /// Attach a coverage report so the serialized JSON carries a
    /// top-level `coverage` key. The CLI populates this when
    /// `--coverage` is passed.
    pub fn with_coverage(mut self, coverage: CoverageReport) -> Self {
        self.coverage = Some(coverage);
        self
    }
}

/// Wire shape combining the canonical [`Report`] with an optional
/// coverage sidecar. Serialized only when a coverage report is attached;
/// without one the reporter writes the bare [`Report`] verbatim so older
/// consumers stay forward-compatible.
#[derive(Debug, Serialize)]
struct ReportWithCoverage<'a> {
    #[serde(flatten)]
    report: &'a Report,
    coverage: &'a CoverageReport,
}

impl Reporter for JsonReporter {
    fn name(&self) -> &'static str {
        "json"
    }

    fn render(&self, report: &Report, out: &mut dyn Write) -> std::io::Result<()> {
        let redacted;
        let view: &Report = match &self.redactor {
            Some(r) => {
                redacted = report.redacted(r);
                &redacted
            }
            None => report,
        };
        // Serialize through a Value so the coverage sidecar can be folded in
        // and the field order stays stable across serde versions.
        let value: Value = match &self.coverage {
            Some(c) => {
                let combined = ReportWithCoverage {
                    report: view,
                    coverage: c,
                };
                serde_json::to_value(&combined).map_err(std::io::Error::other)?
            }
            None => serde_json::to_value(view).map_err(std::io::Error::other)?,
        };
        serde_json::to_writer_pretty(&mut *out, &value).map_err(std::io::Error::other)?;
        writeln!(out)?;
        Ok(())
    }
}

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

    #[test]
    fn json_reporter_outputs_valid_json() {
        let r = Report::sample();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let s = String::from_utf8(buf).unwrap();
        let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
        assert_eq!(parsed["summary"]["passed"], 2);
    }

    #[test]
    fn json_reporter_name_is_stable() {
        assert_eq!(JsonReporter::default().name(), "json");
    }

    #[test]
    fn json_reporter_serializes_timing_fields_verbatim() {
        let r = Report::sample();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        assert_eq!(parsed["summary"]["duration_ms"], 250);
        assert_eq!(parsed["summary"]["cache_hits"], 1);
        assert_eq!(parsed["summary"]["started_at"], "2026-05-15T22:14:03Z");
        let t1 = &parsed["tests"][1];
        assert_eq!(t1["cache_hit"], true);
        assert!(t1["started_at"].is_string());
        assert!(t1["ended_at"].is_string());
    }

    #[test]
    fn json_reporter_redacts_token_shaped_test_name() {
        let mut r = Report::sample();
        r.tests[0].name = "ghp_abcdefghijklmnop1234".into();
        let reporter = JsonReporter::default().with_redactor(Arc::new(Redactor::new()));
        let mut buf = Vec::new();
        reporter.render(&r, &mut buf).unwrap();
        let s = String::from_utf8(buf).unwrap();
        assert!(!s.contains("ghp_abcdefghijklmnop1234"));
        assert!(s.contains("[REDACTED:token]"));
    }

    #[test]
    fn json_reporter_serializes_full_step_timing_tree() {
        let r = Report::sample_stepwise();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        let steps = parsed["tests"][0]["steps"].as_array().expect("steps array");
        assert_eq!(steps.len(), 3);
        assert_eq!(steps[0]["name"], "create_issue");
        assert_eq!(steps[0]["duration_ms"], 12);
        assert_eq!(steps[2]["cache_hit"], true);
        assert_eq!(steps[2]["duration_ms"], 1);
    }

    #[test]
    fn json_reporter_serializes_run_cost_when_present() {
        use crate::report::RunCost;
        let mut r = Report::sample();
        r.cost = Some(RunCost {
            total_cost_usd: 0.42,
            cost_per_juror_avg: 0.0525,
            cost_per_test_avg: 0.21,
            juror_calls: 8,
            tests_with_cost: 2,
            cost_per_completed_task: Some(0.21),
        });
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        let cost = &parsed["cost"];
        assert_eq!(cost["total_cost_usd"], 0.42);
        assert_eq!(cost["cost_per_juror_avg"], 0.0525);
        assert_eq!(cost["cost_per_test_avg"], 0.21);
        assert_eq!(cost["juror_calls"], 8);
        assert_eq!(cost["tests_with_cost"], 2);
    }

    #[test]
    fn json_reporter_omits_run_cost_when_absent() {
        let r = Report::sample();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        assert!(parsed.get("cost").is_none());
    }

    #[test]
    fn json_reporter_attaches_coverage_sidecar_when_set() {
        use crate::coverage::CoverageReport;
        let r = Report::sample();
        let coverage = CoverageReport::sample();
        let mut buf = Vec::new();
        JsonReporter::default()
            .with_coverage(coverage)
            .render(&r, &mut buf)
            .unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        let cov = &parsed["coverage"];
        assert!(cov.is_object(), "coverage key must be present");
        assert_eq!(cov["tool_coverage_pct"], 50.0);
        // Run-level fields still ride along untouched.
        assert_eq!(parsed["summary"]["passed"], 2);
    }

    #[test]
    fn json_reporter_omits_coverage_key_when_not_set() {
        let r = Report::sample();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        assert!(parsed.get("coverage").is_none());
    }

    #[test]
    fn json_reporter_serializes_full_jury_consensus() {
        use crate::report::JuryConsensus;
        let mut r = Report::sample();
        r.tests[0].consensus = Some(JuryConsensus::sample());
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        let jurors = parsed["tests"][0]["consensus"]["jurors"]
            .as_array()
            .expect("jurors array");
        assert_eq!(jurors.len(), 3);
        assert_eq!(jurors[0]["provider"], "openai");
        assert_eq!(jurors[0]["verdict"], "pass");
        // Reasoning is full text in JSON (no pretty truncation).
        let reasoning = jurors[0]["reasoning"].as_str().unwrap();
        assert!(reasoning.contains("Response cites"));
        assert_eq!(jurors[2]["status"], "timeout");
        assert_eq!(jurors[2]["error"], "request timed out after 15s");
    }

    #[test]
    fn json_reporter_serializes_run_metadata_block_when_attached() {
        use crate::report::RunMetadata;
        let mut r = Report::sample();
        r.metadata = Some(RunMetadata::sample());
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        let meta = &parsed["metadata"];
        assert!(meta.is_object(), "metadata block must serialize as object");
        assert_eq!(meta["mcptest_version"], "1.0.0");
        assert_eq!(meta["repo"], "https://github.com/acme/my-mcp-server");
        assert_eq!(meta["branch"], "main");
        assert_eq!(meta["runner_environment"], "github-actions");
        assert_eq!(meta["os"], "linux");
        assert_eq!(meta["arch"], "x86_64");
        assert_eq!(meta["run_id"].as_str().unwrap().len(), 36);
    }

    #[test]
    fn json_reporter_omits_metadata_key_when_not_set() {
        let r = Report::sample();
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        assert!(parsed.get("metadata").is_none());
    }

    #[test]
    fn json_reporter_renders_runner_attached_consensus() {
        // Same shape as the sample-driven test above, but the
        // consensus comes from the runner's attach helper rather than
        // JuryConsensus::sample(). This guards against the reporter
        // accidentally only working against the test fixture.
        use crate::eval::jury::engine::{ConsensusMethod, JurorVote, Vote};
        use crate::eval::provider::ProviderId;
        use crate::report::Verdict;
        use crate::runner::attach_to_test;

        let mut r = Report::sample();
        let votes = vec![
            JurorVote {
                provider: ProviderId::Anthropic,
                model: "claude-haiku-4".into(),
                latency_ms: 600,
                cost_usd: Some(0.00012),
                tokens: None,
                weight: 1.0,
                result: Ok(Vote {
                    verdict: Verdict::Pass,
                    score: Some(0.95),
                    reasoning: Some("rubric met".into()),
                }),
            },
            JurorVote {
                provider: ProviderId::Openai,
                model: "gpt-4o-mini".into(),
                latency_ms: 700,
                cost_usd: Some(0.00010),
                tokens: None,
                weight: 1.0,
                result: Ok(Vote {
                    verdict: Verdict::Pass,
                    score: Some(0.85),
                    reasoning: Some("rubric met".into()),
                }),
            },
        ];
        attach_to_test(&mut r.tests[0], &votes, ConsensusMethod::Unanimous);
        let mut buf = Vec::new();
        JsonReporter::default().render(&r, &mut buf).unwrap();
        let parsed: serde_json::Value =
            serde_json::from_str(&String::from_utf8(buf).unwrap()).unwrap();
        assert_eq!(parsed["tests"][0]["consensus"]["verdict"], "pass");
        let jurors = parsed["tests"][0]["consensus"]["jurors"]
            .as_array()
            .expect("jurors array");
        assert_eq!(jurors.len(), 2);
        assert_eq!(jurors[0]["provider"], "anthropic");
        assert_eq!(jurors[1]["provider"], "openai");
    }
}
