standout 10.0.0

Styled CLI template rendering with automatic terminal detection
use serde::ser::SerializeStruct;
use serde::{Serialize, Serializer};

use super::{Message, MessageLevel};
use crate::cli::{ExitStatus, Output};
use crate::tabular::TabularSpec;
use crate::ContractSurface;

#[derive(Debug, Clone)]
pub struct ListViewResult<T> {
    pub items: Vec<T>,
    pub empty_exit_status: Option<ExitStatus>,
    pub intro: Option<String>,
    pub ending: Option<String>,
    pub messages: Vec<Message>,
    pub total_count: Option<usize>,
    pub filter_summary: Option<String>,
    pub tabular_spec: Option<TabularSpec>,
}

impl<T> ContractSurface for ListViewResult<T> {
    const SCHEMA_VERSION: u32 = 1;
}

impl<T: Serialize> Serialize for ListViewResult<T> {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        let present = [
            self.intro.is_some(),
            self.ending.is_some(),
            !self.messages.is_empty(),
            self.total_count.is_some(),
            self.filter_summary.is_some(),
            self.tabular_spec.is_some(),
        ]
        .iter()
        .filter(|set| **set)
        .count();
        let mut document = serializer.serialize_struct("ListViewResult", 2 + present)?;
        document.serialize_field("schema_version", &Self::SCHEMA_VERSION)?;
        document.serialize_field("items", &self.items)?;
        if let Some(intro) = &self.intro {
            document.serialize_field("intro", intro)?;
        }
        if let Some(ending) = &self.ending {
            document.serialize_field("ending", ending)?;
        }
        if !self.messages.is_empty() {
            document.serialize_field("messages", &self.messages)?;
        }
        if let Some(total_count) = &self.total_count {
            document.serialize_field("total_count", total_count)?;
        }
        if let Some(filter_summary) = &self.filter_summary {
            document.serialize_field("filter_summary", filter_summary)?;
        }
        if let Some(tabular_spec) = &self.tabular_spec {
            document.serialize_field("tabular_spec", tabular_spec)?;
        }
        document.end()
    }
}

impl<T> ListViewResult<T> {
    pub fn new(items: Vec<T>) -> Self {
        Self {
            items,
            empty_exit_status: None,
            intro: None,
            ending: None,
            messages: Vec::new(),
            total_count: None,
            filter_summary: None,
            tabular_spec: None,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    pub fn len(&self) -> usize {
        self.items.len()
    }
}

impl<T: Serialize> ListViewResult<T> {
    /// `Output::Render(self)`, carrying `empty_exit_status` when the list is empty.
    pub fn into_output(self) -> Output<Self> {
        match self.empty_exit_status.filter(|_| self.items.is_empty()) {
            Some(status) => Output::Render(self).with_exit_status(status),
            None => Output::Render(self),
        }
    }
}

impl<T> Default for ListViewResult<T> {
    fn default() -> Self {
        Self::new(Vec::new())
    }
}

#[derive(Debug)]
pub struct ListViewBuilder<T> {
    items: Vec<T>,
    empty_exit_status: Option<ExitStatus>,
    intro: Option<String>,
    ending: Option<String>,
    messages: Vec<Message>,
    total_count: Option<usize>,
    filter_summary: Option<String>,
    tabular_spec: Option<TabularSpec>,
}

impl<T> ListViewBuilder<T> {
    pub fn new(items: impl IntoIterator<Item = T>) -> Self {
        Self {
            items: items.into_iter().collect(),
            empty_exit_status: None,
            intro: None,
            ending: None,
            messages: Vec::new(),
            total_count: None,
            filter_summary: None,
            tabular_spec: None,
        }
    }

    /// The list still renders; the status is a signal, not a failure.
    pub fn empty_exit_status(mut self, status: impl Into<ExitStatus>) -> Self {
        self.empty_exit_status = Some(status.into());
        self
    }

    pub fn intro(mut self, text: impl Into<String>) -> Self {
        self.intro = Some(text.into());
        self
    }

    pub fn ending(mut self, text: impl Into<String>) -> Self {
        self.ending = Some(text.into());
        self
    }

    pub fn message(mut self, level: MessageLevel, text: impl Into<String>) -> Self {
        self.messages.push(Message::new(level, text));
        self
    }

    pub fn info(self, text: impl Into<String>) -> Self {
        self.message(MessageLevel::Info, text)
    }

    pub fn success(self, text: impl Into<String>) -> Self {
        self.message(MessageLevel::Success, text)
    }

    pub fn warning(self, text: impl Into<String>) -> Self {
        self.message(MessageLevel::Warning, text)
    }

    pub fn error(self, text: impl Into<String>) -> Self {
        self.message(MessageLevel::Error, text)
    }

    pub fn total_count(mut self, count: usize) -> Self {
        self.total_count = Some(count);
        self
    }

    pub fn filter_summary(mut self, summary: impl Into<String>) -> Self {
        self.filter_summary = Some(summary.into());
        self
    }

    pub fn tabular_spec(mut self, spec: TabularSpec) -> Self {
        self.tabular_spec = Some(spec);
        self
    }

    pub fn build(self) -> ListViewResult<T> {
        ListViewResult {
            items: self.items,
            empty_exit_status: self.empty_exit_status,
            intro: self.intro,
            ending: self.ending,
            messages: self.messages,
            total_count: self.total_count,
            filter_summary: self.filter_summary,
            tabular_spec: self.tabular_spec,
        }
    }
}

impl<T: Serialize> ListViewBuilder<T> {
    /// `build().into_output()` in one step.
    pub fn output(self) -> Output<ListViewResult<T>> {
        self.build().into_output()
    }
}

pub fn list_view<T>(items: impl IntoIterator<Item = T>) -> ListViewBuilder<T> {
    ListViewBuilder::new(items)
}

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

    #[test]
    fn test_list_view_builder_basic() {
        let result = list_view(vec![1, 2, 3]).build();
        assert_eq!(result.items, vec![1, 2, 3]);
        assert!(result.intro.is_none());
        assert!(result.ending.is_none());
        assert!(result.messages.is_empty());
    }

    #[test]
    fn test_list_view_builder_with_intro_ending() {
        let result = list_view(vec!["a", "b"])
            .intro("Header")
            .ending("Footer")
            .build();

        assert_eq!(result.intro, Some("Header".to_string()));
        assert_eq!(result.ending, Some("Footer".to_string()));
    }

    #[test]
    fn test_list_view_builder_with_messages() {
        let result = list_view(Vec::<i32>::new())
            .info("Info message")
            .warning("Warning message")
            .error("Error message")
            .build();

        assert_eq!(result.messages.len(), 3);
        assert_eq!(result.messages[0].level, MessageLevel::Info);
        assert_eq!(result.messages[1].level, MessageLevel::Warning);
        assert_eq!(result.messages[2].level, MessageLevel::Error);
    }

    #[test]
    fn test_list_view_builder_with_filter_info() {
        let result = list_view(vec![1, 2])
            .total_count(10)
            .filter_summary("status=active")
            .build();

        assert_eq!(result.total_count, Some(10));
        assert_eq!(result.filter_summary, Some("status=active".to_string()));
    }

    #[test]
    fn test_list_view_result_len_and_empty() {
        let empty: ListViewResult<i32> = list_view(vec![]).build();
        assert!(empty.is_empty());
        assert_eq!(empty.len(), 0);

        let non_empty = list_view(vec![1, 2, 3]).build();
        assert!(!non_empty.is_empty());
        assert_eq!(non_empty.len(), 3);
    }

    #[test]
    fn test_list_view_serialization() {
        let result = list_view(vec!["item1", "item2"])
            .intro("Header")
            .warning("Watch out")
            .build();

        let json = serde_json::to_string(&result).unwrap();
        assert!(json.starts_with("{\"schema_version\":1,\"items\":[\"item1\",\"item2\"]"));
        assert!(json.contains("\"intro\":\"Header\""));
        assert!(json.contains("\"warning\""));
    }

    #[test]
    fn test_list_view_serialization_skips_empty() {
        let result = list_view(vec![1]).build();
        let json = serde_json::to_string(&result).unwrap();

        assert_eq!(json, "{\"schema_version\":1,\"items\":[1]}");
        assert!(!json.contains("\"intro\""));
        assert!(!json.contains("\"ending\""));
        assert!(!json.contains("\"messages\""));
        assert!(!json.contains("\"total_count\""));
        assert!(!json.contains("\"filter_summary\""));
        assert!(!json.contains("\"tabular_spec\""));
    }

    #[test]
    fn test_list_view_default() {
        let result: ListViewResult<String> = ListViewResult::default();
        assert!(result.is_empty());
    }

    #[test]
    fn empty_exit_status_applies_to_an_empty_list_only() {
        let empty = list_view(Vec::<i32>::new()).empty_exit_status(3).output();
        assert!(empty.is_render());
        assert_eq!(empty.exit_status(), ExitStatus::from(3));

        let filled = list_view(vec![1]).empty_exit_status(3).output();
        assert_eq!(filled.exit_status(), ExitStatus::SUCCESS);

        let undeclared = list_view(Vec::<i32>::new()).output();
        assert_eq!(undeclared.exit_status(), ExitStatus::SUCCESS);

        let json =
            serde_json::to_string(&list_view(Vec::<i32>::new()).empty_exit_status(3).build())
                .unwrap();
        assert!(!json.contains("exit_status"), "{json}");
    }
}