//! Protocol-violation section of the run [`Report`](super::Report).
//!
//! Why this exists (WOR-1311): the protocol client records wire
//! misbehavior (string-id echoes, orphan responses, malformed frames)
//! per connection, but evidence that never reaches the report dies with
//! the connection. This module is the bridge: it folds each server's
//! [`ProtocolViolationSummary`] into flat per-(server, kind) rows the
//! reporters can render and the JSON artifact can carry.

use serde::{Deserialize, Serialize};

use crate::protocol::ProtocolViolationSummary;

/// One protocol-violation finding on the run report: a (server, kind)
/// pair with its exact count and one retained example. Flat and
/// string-typed so the JSON artifact stays stable even if the protocol
/// layer grows new violation kinds.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ProtocolViolationReport {
    /// Suite-local name of the server connection that misbehaved.
    pub server: String,
    /// Violation kind, as the protocol layer's display name (for
    /// example `string-id echo`).
    pub kind: String,
    /// Exact count of violations of this kind on this connection,
    /// including any whose examples were evicted from the capped ring.
    pub count: u64,
    /// One retained example detail, printable verbatim. Empty when
    /// every example of this kind was evicted by later violations.
    pub example: String,
}

/// Fold per-server violation summaries into report rows, one per
/// (server, kind) with a non-zero count. Servers arrive sorted from
/// [`ServerPool::protocol_violations`](crate::connector::ServerPool::protocol_violations),
/// and kinds are in enum order, so the output is deterministic.
pub fn protocol_violation_entries(
    per_server: &[(String, ProtocolViolationSummary)],
) -> Vec<ProtocolViolationReport> {
    let mut out = Vec::new();
    for (server, summary) in per_server {
        for (kind, count) in &summary.kind_counts {
            if *count == 0 {
                continue;
            }
            let example = summary
                .examples
                .iter()
                .find(|v| v.kind == *kind)
                .map(|v| v.detail.clone())
                .unwrap_or_default();
            out.push(ProtocolViolationReport {
                server: server.clone(),
                kind: kind.to_string(),
                count: *count,
                example,
            });
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::protocol::{ProtocolViolation, ProtocolViolationKind};

    fn summary_with(
        kind_counts: Vec<(ProtocolViolationKind, u64)>,
        examples: Vec<(ProtocolViolationKind, &str)>,
    ) -> ProtocolViolationSummary {
        ProtocolViolationSummary {
            total: kind_counts.iter().map(|(_, n)| n).sum(),
            examples: examples
                .into_iter()
                .map(|(kind, detail)| ProtocolViolation {
                    kind,
                    detail: detail.to_string(),
                })
                .collect(),
            kind_counts,
        }
    }

    #[test]
    fn entries_carry_one_row_per_server_and_kind_with_an_example() {
        let per_server = vec![
            ("clean".to_string(), summary_with(Vec::new(), Vec::new())),
            (
                "github".to_string(),
                summary_with(
                    vec![
                        (ProtocolViolationKind::StringIdEcho, 3),
                        (ProtocolViolationKind::MalformedFrame, 1),
                    ],
                    vec![
                        (ProtocolViolationKind::StringIdEcho, "echoed 1 as \"1\""),
                        (
                            ProtocolViolationKind::MalformedFrame,
                            "both result and error",
                        ),
                    ],
                ),
            ),
        ];
        let entries = protocol_violation_entries(&per_server);
        assert_eq!(entries.len(), 2, "clean server contributes no rows");
        assert_eq!(entries[0].server, "github");
        assert_eq!(entries[0].kind, "string-id echo");
        assert_eq!(entries[0].count, 3);
        assert_eq!(entries[0].example, "echoed 1 as \"1\"");
        assert_eq!(entries[1].kind, "malformed frame");
        assert_eq!(entries[1].count, 1);
    }

    #[test]
    fn an_evicted_kind_still_reports_its_count_with_an_empty_example() {
        let per_server = vec![(
            "srv".to_string(),
            summary_with(
                vec![(ProtocolViolationKind::OrphanResponse, 40)],
                Vec::new(),
            ),
        )];
        let entries = protocol_violation_entries(&per_server);
        assert_eq!(entries.len(), 1);
        assert_eq!(entries[0].count, 40);
        assert_eq!(entries[0].example, "");
    }
}
