//! Report aggregation and rendering for the labeler conformance suite.

use std::fmt;
use std::io;
use std::time::Instant;

use miette::{Diagnostic, GraphicalReportHandler, GraphicalTheme};

/// The five rendering severities for a check result.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckStatus {
    /// All checks passed — renders as `[OK]`.
    Pass,
    /// Specification violation — renders as `[FAIL]`.
    SpecViolation,
    /// Network error — renders as `[NET]`.
    NetworkError,
    /// Advisory warning — renders as `[WARN]`.
    Advisory,
    /// Check skipped (not yet implemented or blocked by earlier failure) — renders as `[SKIP]`.
    Skipped,
}

impl CheckStatus {
    /// Plain-text glyph for this status.
    pub fn glyph(self) -> &'static str {
        match self {
            CheckStatus::Pass => "[OK]",
            CheckStatus::SpecViolation => "[FAIL]",
            CheckStatus::NetworkError => "[NET]",
            CheckStatus::Advisory => "[WARN]",
            CheckStatus::Skipped => "[SKIP]",
        }
    }

    /// Glyph wrapped in an ANSI SGR sequence for this status, or the
    /// plain glyph when `no_color` is true. Colors are chosen to give
    /// each severity a distinct visual weight in a terminal:
    ///
    /// * `Pass` — bold green.
    /// * `SpecViolation` — bold red.
    /// * `NetworkError` — bold magenta (distinct from `Advisory` so
    ///   reachability failures don't blur into spec warnings).
    /// * `Advisory` — bold yellow.
    /// * `Skipped` — dim.
    pub fn styled_glyph(self, no_color: bool) -> &'static str {
        if no_color {
            return self.glyph();
        }
        match self {
            CheckStatus::Pass => "\x1b[1;32m[OK]\x1b[0m",
            CheckStatus::SpecViolation => "\x1b[1;31m[FAIL]\x1b[0m",
            CheckStatus::NetworkError => "\x1b[1;35m[NET]\x1b[0m",
            CheckStatus::Advisory => "\x1b[1;33m[WARN]\x1b[0m",
            CheckStatus::Skipped => "\x1b[2m[SKIP]\x1b[0m",
        }
    }
}

impl fmt::Display for CheckStatus {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.glyph())
    }
}

/// Result of a single check within a labeler validation stage.
#[derive(Debug)]
pub struct CheckResult {
    /// Stable identifier for this check (e.g., "identity::target_resolved").
    pub id: &'static str,
    /// Which stage this check belongs to.
    pub stage: Stage,
    /// The outcome severity of this check.
    pub status: CheckStatus,
    /// Human-readable summary of what was checked.
    pub summary: std::borrow::Cow<'static, str>,
    /// Optional diagnostic with source code context (for failures).
    pub diagnostic: Option<Box<dyn Diagnostic + Send + Sync>>,
    /// Optional reason why a check was skipped.
    pub skipped_reason: Option<std::borrow::Cow<'static, str>>,
}

/// The stages of labeler validation.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Stage {
    /// DID document and labeler record validation.
    Identity,
    /// HTTP endpoint healthchecks.
    Http,
    /// WebSocket subscription validation.
    Subscription,
    /// Cryptographic signing verification.
    Crypto,
}

impl Stage {
    /// Human-readable heading for this stage.
    pub fn label(self) -> &'static str {
        match self {
            Stage::Identity => "Identity",
            Stage::Http => "HTTP",
            Stage::Subscription => "Subscription",
            Stage::Crypto => "Crypto",
        }
    }
}

/// Summary counts of check results by severity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SummaryCounts {
    pub pass: usize,
    pub spec_violation: usize,
    pub network_error: usize,
    pub advisory: usize,
    pub skipped: usize,
}

impl SummaryCounts {
    /// Count results by severity.
    pub fn from_results(results: &[CheckResult]) -> Self {
        let mut counts = SummaryCounts {
            pass: 0,
            spec_violation: 0,
            network_error: 0,
            advisory: 0,
            skipped: 0,
        };

        for result in results {
            match result.status {
                CheckStatus::Pass => counts.pass += 1,
                CheckStatus::SpecViolation => counts.spec_violation += 1,
                CheckStatus::NetworkError => counts.network_error += 1,
                CheckStatus::Advisory => counts.advisory += 1,
                CheckStatus::Skipped => counts.skipped += 1,
            }
        }

        counts
    }
}

/// Header information for a labeler report.
#[derive(Debug, Clone)]
pub struct ReportHeader {
    /// The input target (handle, DID, or URL).
    pub target: String,
    /// The resolved DID if applicable.
    pub resolved_did: Option<String>,
    /// The PDS endpoint if resolved.
    pub pds_endpoint: Option<String>,
    /// The labeler service endpoint if resolved.
    pub labeler_endpoint: Option<String>,
}

/// Configuration for rendering the report.
#[derive(Debug, Clone)]
pub struct RenderConfig {
    /// Whether to suppress colored output.
    pub no_color: bool,
}

/// The complete labeler validation report.
#[derive(Debug)]
pub struct LabelerReport {
    /// Header with target and resolved endpoints.
    pub header: ReportHeader,
    /// All validation results collected during the run.
    pub results: Vec<CheckResult>,
    /// When the run started.
    pub started_at: Instant,
    /// When the run finished.
    pub finished_at: Option<Instant>,
}

impl LabelerReport {
    /// Create a new empty report.
    pub fn new(header: ReportHeader) -> Self {
        LabelerReport {
            header,
            results: Vec::new(),
            started_at: Instant::now(),
            finished_at: None,
        }
    }

    /// Record a check result.
    pub fn record(&mut self, result: CheckResult) {
        self.results.push(result);
    }

    /// Mark the report as finished.
    pub fn finish(&mut self) {
        self.finished_at = Some(Instant::now());
    }

    /// Compute the exit code:
    ///
    /// * `1` if any check is a `SpecViolation` — the labeler is
    ///   reachable but does not conform to the spec.
    /// * `2` if there is no `SpecViolation` but at least one
    ///   `NetworkError` — the run could not fully exercise the labeler
    ///   (DNS, HTTP, or WebSocket failure), so the result is
    ///   inconclusive and the operator should investigate.
    /// * `0` otherwise.
    ///
    /// Spec violations take precedence over network errors so that a
    /// run that uncovered a real conformance bug still surfaces as
    /// such even if some other stage was unreachable.
    pub fn exit_code(&self) -> i32 {
        let mut has_spec_violation = false;
        let mut has_network_error = false;
        for r in &self.results {
            match r.status {
                CheckStatus::SpecViolation => has_spec_violation = true,
                CheckStatus::NetworkError => has_network_error = true,
                _ => {}
            }
        }
        if has_spec_violation {
            1
        } else if has_network_error {
            2
        } else {
            0
        }
    }

    /// Get summary counts of all results.
    pub fn summary_counts(&self) -> SummaryCounts {
        SummaryCounts::from_results(&self.results)
    }

    /// Render the report to the given writer.
    pub fn render<W: io::Write>(&self, out: &mut W, config: &RenderConfig) -> io::Result<()> {
        // Header line with target and resolved endpoints.
        let elapsed = self
            .finished_at
            .map(|f| f.duration_since(self.started_at).as_millis())
            .unwrap_or(0);
        writeln!(out, "Target: {}", self.header.target)?;
        if let Some(did) = &self.header.resolved_did {
            writeln!(out, "  Resolved DID: {did}")?;
        }
        if let Some(pds) = &self.header.pds_endpoint {
            writeln!(out, "  PDS endpoint: {pds}")?;
        }
        if let Some(labeler) = &self.header.labeler_endpoint {
            writeln!(out, "  Labeler endpoint: {labeler}")?;
        }
        writeln!(out, "  elapsed: {elapsed}ms")?;
        writeln!(out)?;

        // Group results by stage and render.
        let mut current_stage: Option<Stage> = None;
        for result in &self.results {
            if Some(result.stage) != current_stage {
                current_stage = Some(result.stage);
                writeln!(out, "== {} ==", result.stage.label())?;
            }

            // Write the check result line.
            write!(
                out,
                "{} {} ",
                result.status.styled_glyph(config.no_color),
                result.summary
            )?;
            if let Some(reason) = &result.skipped_reason {
                write!(out, "— {reason}")?;
            }
            writeln!(out)?;

            // Render diagnostic if present (and not skipped).
            if let Some(diag) = &result.diagnostic {
                if result.status != CheckStatus::Skipped {
                    let theme = if config.no_color {
                        GraphicalTheme::unicode_nocolor()
                    } else {
                        GraphicalTheme::default()
                    };
                    let handler = GraphicalReportHandler::new().with_theme(theme);
                    let mut buf = String::new();
                    // Render the diagnostic to a string buffer.
                    if let Err(_e) = handler.render_report(&mut buf, diag.as_ref()) {
                        // If rendering fails, write a fallback message.
                        writeln!(out, "  (diagnostic rendering failed)")?;
                    } else {
                        for line in buf.lines() {
                            writeln!(out, "  {line}")?;
                        }
                    }
                }
            }
        }

        writeln!(out)?;

        // Summary footer.
        let counts = self.summary_counts();
        write!(
            out,
            "Summary: {} passed, {} failed (spec), {} network errors, {} advisories, {} skipped. ",
            counts.pass,
            counts.spec_violation,
            counts.network_error,
            counts.advisory,
            counts.skipped
        )?;
        writeln!(out, "Exit code: {}", self.exit_code())?;

        Ok(())
    }
}

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

    #[test]
    fn exit_code_only_advisory_is_zero() {
        let header = ReportHeader {
            target: "test".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);
        report.record(CheckResult {
            id: "test",
            stage: Stage::Identity,
            status: CheckStatus::Advisory,
            summary: "advisory check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        assert_eq!(report.exit_code(), 0);
    }

    #[test]
    fn exit_code_only_network_errors_is_two() {
        let header = ReportHeader {
            target: "test".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);
        report.record(CheckResult {
            id: "test",
            stage: Stage::Identity,
            status: CheckStatus::NetworkError,
            summary: "network check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        assert_eq!(report.exit_code(), 2);
    }

    #[test]
    fn exit_code_spec_violation_takes_precedence_over_network_error() {
        let header = ReportHeader {
            target: "test".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);
        report.record(CheckResult {
            id: "net",
            stage: Stage::Identity,
            status: CheckStatus::NetworkError,
            summary: "network check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        report.record(CheckResult {
            id: "spec",
            stage: Stage::Identity,
            status: CheckStatus::SpecViolation,
            summary: "spec check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        assert_eq!(report.exit_code(), 1);
    }

    #[test]
    fn exit_code_with_spec_violation_is_one() {
        let header = ReportHeader {
            target: "test".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);
        report.record(CheckResult {
            id: "test",
            stage: Stage::Identity,
            status: CheckStatus::SpecViolation,
            summary: "spec check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        assert_eq!(report.exit_code(), 1);
    }

    #[test]
    fn summary_counts_partition_correct() {
        let header = ReportHeader {
            target: "test".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);

        report.record(CheckResult {
            id: "test1",
            stage: Stage::Identity,
            status: CheckStatus::Pass,
            summary: "pass check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test2",
            stage: Stage::Identity,
            status: CheckStatus::SpecViolation,
            summary: "fail check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test3",
            stage: Stage::Http,
            status: CheckStatus::NetworkError,
            summary: "net check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test4",
            stage: Stage::Http,
            status: CheckStatus::Advisory,
            summary: "warn check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test5",
            stage: Stage::Subscription,
            status: CheckStatus::Skipped,
            summary: "skip check".into(),
            diagnostic: None,
            skipped_reason: Some("not implemented".into()),
        });

        let counts = report.summary_counts();
        assert_eq!(counts.pass, 1);
        assert_eq!(counts.spec_violation, 1);
        assert_eq!(counts.network_error, 1);
        assert_eq!(counts.advisory, 1);
        assert_eq!(counts.skipped, 1);
    }

    #[test]
    fn render_basic_glyphs() {
        let header = ReportHeader {
            target: "test.example".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);

        report.record(CheckResult {
            id: "test1",
            stage: Stage::Identity,
            status: CheckStatus::Pass,
            summary: "pass check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test2",
            stage: Stage::Identity,
            status: CheckStatus::SpecViolation,
            summary: "fail check".into(),
            diagnostic: None,
            skipped_reason: None,
        });

        report.record(CheckResult {
            id: "test3",
            stage: Stage::Http,
            status: CheckStatus::Skipped,
            summary: "skip check".into(),
            diagnostic: None,
            skipped_reason: Some("not yet implemented".into()),
        });

        report.finish();

        let mut buf = Vec::new();
        let config = RenderConfig { no_color: true };
        report.render(&mut buf, &config).expect("render failed");

        let output = String::from_utf8(buf).expect("invalid utf-8");

        // Check for the expected glyphs.
        assert!(
            output.contains("[OK]"),
            "output should contain [OK] glyph:\n{output}"
        );
        assert!(
            output.contains("[FAIL]"),
            "output should contain [FAIL] glyph:\n{output}"
        );
        assert!(
            output.contains("[SKIP]"),
            "output should contain [SKIP] glyph:\n{output}"
        );
        // With `no_color: true` there must be no ANSI SGR escapes.
        assert!(
            !output.contains('\x1b'),
            "no_color output should not contain ANSI escapes:\n{output}"
        );
    }

    #[test]
    fn styled_glyph_emits_ansi_when_color_enabled() {
        // Color on: each status wraps its glyph in an ANSI SGR sequence
        // and closes with the reset escape.
        for status in [
            CheckStatus::Pass,
            CheckStatus::SpecViolation,
            CheckStatus::NetworkError,
            CheckStatus::Advisory,
            CheckStatus::Skipped,
        ] {
            let colored = status.styled_glyph(false);
            assert!(
                colored.starts_with("\x1b[") && colored.ends_with("\x1b[0m"),
                "{status:?} colored form must be wrapped in SGR escapes: {colored:?}"
            );
            assert!(
                colored.contains(status.glyph()),
                "{status:?} colored form must contain the plain glyph"
            );
        }
    }

    #[test]
    fn render_with_color_wraps_glyphs_in_ansi() {
        // End-to-end: render() with `no_color: false` must emit colored
        // glyphs — this is the load-bearing path for the terminal UX.
        let header = ReportHeader {
            target: "test.example".to_string(),
            resolved_did: None,
            pds_endpoint: None,
            labeler_endpoint: None,
        };
        let mut report = LabelerReport::new(header);
        report.record(CheckResult {
            id: "test1",
            stage: Stage::Identity,
            status: CheckStatus::Pass,
            summary: "pass check".into(),
            diagnostic: None,
            skipped_reason: None,
        });
        report.finish();

        let mut buf = Vec::new();
        report
            .render(&mut buf, &RenderConfig { no_color: false })
            .expect("render failed");
        let output = String::from_utf8(buf).expect("invalid utf-8");
        assert!(
            output.contains(CheckStatus::Pass.styled_glyph(false)),
            "colored output should contain the colored [OK] glyph:\n{output}"
        );
    }
}
