//! Structured friendly-error payload (ADR-0019 §7).
//!
//! `FriendlyError` is the type the translator produces. The
//! renderer (in this module) composes it into final text. The
//! payload is structured so the renderer can decide layout —
//! per ADR-0019 §F-detail, "leave rendering details to the
//! renderer instead of banging everything in one string".
//!
//! Composition order:
//!
//! ```text
//! <headline>
//!
//! <hint>                 (only when present)
//!
//! ┌────────────┬───────┐ (only when present)
//! │ … bordered │ table │
//! └────────────┴───────┘
//! ```
//!
//! Blank-line separators sit between the three blocks. The
//! diagnostic table uses ADR-0017's bordered renderer (via
//! `output_render::render_diagnostic_table`) so its visual
//! style matches the lossy / incompatible refusal diagnostics
//! introduced there.

use crate::output_render::{Alignment, render_diagnostic_table};

/// Bordered row pinpoint produced by the translator's
/// post-failure re-query (ADR-0019 §6). The renderer hands
/// these straight to ADR-0017's `render_diagnostic_table`.
#[derive(Debug, Clone)]
pub struct DiagnosticTable {
    pub headers: Vec<String>,
    pub rows: Vec<Vec<String>>,
    pub alignments: Vec<Alignment>,
}

/// Structured friendly-error payload (ADR-0019 §7).
///
/// `headline` is always present — a single, complete line of
/// "what happened". `hint` is the verbose-mode addition: one or
/// more lines of pedagogical "what to do next". `diagnostic_table`
/// is the (optional) row pinpoint surfaced through ADR-0017's
/// bordered renderer.
#[derive(Debug, Clone)]
pub struct FriendlyError {
    pub headline: String,
    pub hint: Option<String>,
    pub diagnostic_table: Option<DiagnosticTable>,
}

impl FriendlyError {
    /// Compose the payload into a single multi-line `String`.
    /// Newline-separated; safe to feed into `note_error()`,
    /// `eprintln!`, or any other plain-text sink.
    #[must_use]
    pub fn render(&self) -> String {
        self.render_lines().join("\n")
    }

    /// Compose the payload into a sequence of display lines.
    /// Used by the App layer where each output line is its own
    /// `OutputLine` for accurate scroll-position math (per the
    /// `App::push_multiline` invariant).
    #[must_use]
    pub fn render_lines(&self) -> Vec<String> {
        let mut out: Vec<String> = Vec::new();
        // Headline can itself be multi-line if the catalog
        // entry has embedded newlines (rare today; possible
        // for future contributions). Split so each display
        // line is its own entry.
        for line in self.headline.lines() {
            out.push(line.to_string());
        }
        if let Some(hint) = &self.hint {
            out.push(String::new());
            for line in hint.lines() {
                out.push(line.to_string());
            }
        }
        if let Some(table) = &self.diagnostic_table {
            out.push(String::new());
            out.extend(render_diagnostic_table(
                &table.headers,
                &table.rows,
                &table.alignments,
            ));
        }
        out
    }
}

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

    fn fe(headline: &str) -> FriendlyError {
        FriendlyError {
            headline: headline.to_string(),
            hint: None,
            diagnostic_table: None,
        }
    }

    #[test]
    fn renders_headline_only_when_no_hint_no_table() {
        let f = fe("`Customers.id` already has the value `5`.");
        let s = f.render();
        assert_eq!(s, "`Customers.id` already has the value `5`.");
    }

    #[test]
    fn renders_headline_blank_then_hint() {
        let f = FriendlyError {
            headline: "`Customers.id` already has the value `5`.".to_string(),
            hint: Some("Pick a different value or update the existing row.".to_string()),
            diagnostic_table: None,
        };
        let lines = f.render_lines();
        assert_eq!(
            lines,
            vec![
                "`Customers.id` already has the value `5`.".to_string(),
                String::new(),
                "Pick a different value or update the existing row.".to_string(),
            ],
        );
    }

    #[test]
    fn renders_headline_blank_table_with_no_hint() {
        let f = FriendlyError {
            headline: "rows still reference this".to_string(),
            hint: None,
            diagnostic_table: Some(DiagnosticTable {
                headers: vec!["id".to_string(), "name".to_string()],
                rows: vec![
                    vec!["1".to_string(), "Alice".to_string()],
                    vec!["2".to_string(), "Bob".to_string()],
                ],
                alignments: vec![Alignment::Right, Alignment::Left],
            }),
        };
        let lines = f.render_lines();
        // headline + blank + bordered table (5+ lines).
        assert_eq!(lines[0], "rows still reference this");
        assert_eq!(lines[1], "");
        // The first table line is the top border.
        assert!(
            lines[2].starts_with('┌'),
            "expected bordered table after headline, got: {:?}",
            lines[2]
        );
        // And the headers contain `id` and `name`.
        assert!(
            lines.iter().any(|l| l.contains("id") && l.contains("name")),
            "missing headers row: {lines:?}"
        );
    }

    #[test]
    fn renders_all_three_with_blank_separators() {
        let f = FriendlyError {
            headline: "headline".to_string(),
            hint: Some("hint".to_string()),
            diagnostic_table: Some(DiagnosticTable {
                headers: vec!["c".to_string()],
                rows: vec![vec!["v".to_string()]],
                alignments: vec![Alignment::Left],
            }),
        };
        let lines = f.render_lines();
        // headline, blank, hint, blank, then 5 lines of table
        // = 9 minimum. We assert order rather than exact length
        // so future renderer tweaks don't trip the test.
        assert_eq!(lines[0], "headline");
        assert_eq!(lines[1], "");
        assert_eq!(lines[2], "hint");
        assert_eq!(lines[3], "");
        assert!(lines[4].starts_with('┌'));
    }

    #[test]
    fn multi_line_headline_splits_at_newlines() {
        // Future-proofing: if a catalog entry ever embeds a
        // newline in the headline, the renderer treats each
        // line independently rather than emitting one long row.
        let f = fe("first line\nsecond line");
        let lines = f.render_lines();
        assert_eq!(lines[0], "first line");
        assert_eq!(lines[1], "second line");
    }
}
