//! Terminal color styling for the human-readable renderers.
//!
//! Color is opt-in and resolved once at the CLI boundary: `--color always`
//! forces it on, `never` off, and `auto` (the default) turns it on only when
//! the sink is a terminal and `NO_COLOR` is unset. Everything downstream
//! renders through a [`Styler`] that either wraps text in ANSI SGR codes or
//! returns it untouched, so machine formats (JSON, SARIF, ...), redirected
//! output, and captured test output stay plain. We use `anstyle` for the SGR
//! definitions (the same styling primitive clap uses) rather than hand-rolled
//! escape codes, and centralize the palette here so the renderers name a role
//! (`pass`, `fail`, `warn`) instead of assembling styles ad hoc.

use anstyle::{AnsiColor, Style};

/// Whether to colorize human-readable output. Mirrors the `--color` flag.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ColorChoice {
    /// Color only when the sink is a terminal and `NO_COLOR` is unset.
    #[default]
    Auto,
    /// Always color, even when piped.
    Always,
    /// Never color.
    Never,
}

impl ColorChoice {
    /// Resolve to an on/off decision.
    ///
    /// `is_terminal` is whether the output sink is a TTY (the caller passes
    /// `std::io::stdout().is_terminal()`). The `NO_COLOR` convention
    /// (<https://no-color.org>) forces color off under `Auto` regardless of the
    /// terminal; `Always` overrides it (the user asked explicitly).
    pub fn resolve(self, is_terminal: bool) -> bool {
        match self {
            ColorChoice::Always => true,
            ColorChoice::Never => false,
            ColorChoice::Auto => is_terminal && std::env::var_os("NO_COLOR").is_none(),
        }
    }
}

/// Paints text with ANSI styles, or returns it unchanged when color is off.
///
/// Construct one with the resolved on/off from [`ColorChoice::resolve`] and
/// hand it to the renderers. A disabled styler is a pure passthrough, so the
/// same render path serves both colored terminals and plain files.
#[derive(Debug, Clone, Copy)]
pub struct Styler {
    enabled: bool,
}

impl Styler {
    /// Build a styler from a resolved on/off decision.
    pub fn new(enabled: bool) -> Self {
        Self { enabled }
    }

    /// A passthrough styler that never colors. Used by machine formats, the
    /// `--output`-to-file path, and tests.
    pub fn plain() -> Self {
        Self { enabled: false }
    }

    /// True when this styler emits color.
    pub fn enabled(&self) -> bool {
        self.enabled
    }

    /// Wrap `text` in `style` (plus a reset) when enabled; otherwise return it
    /// unchanged.
    pub fn paint(&self, style: Style, text: &str) -> String {
        if self.enabled {
            format!("{}{text}{}", style.render(), style.render_reset())
        } else {
            text.to_string()
        }
    }

    /// A passing verdict: bold green.
    pub fn pass(&self, text: &str) -> String {
        self.paint(Style::new().fg_color(green()).bold(), text)
    }

    /// A failing verdict or a critical/high finding: bold red.
    pub fn fail(&self, text: &str) -> String {
        self.paint(Style::new().fg_color(red()).bold(), text)
    }

    /// A warning or medium-severity signal: yellow.
    pub fn warn(&self, text: &str) -> String {
        self.paint(Style::new().fg_color(yellow()), text)
    }

    /// An identifier or low-severity signal worth a glance: cyan.
    pub fn accent(&self, text: &str) -> String {
        self.paint(Style::new().fg_color(cyan()), text)
    }

    /// Secondary or informational text: dimmed.
    pub fn dim(&self, text: &str) -> String {
        self.paint(Style::new().dimmed(), text)
    }
}

fn green() -> Option<anstyle::Color> {
    Some(AnsiColor::Green.into())
}
fn red() -> Option<anstyle::Color> {
    Some(AnsiColor::Red.into())
}
fn yellow() -> Option<anstyle::Color> {
    Some(AnsiColor::Yellow.into())
}
fn cyan() -> Option<anstyle::Color> {
    Some(AnsiColor::Cyan.into())
}

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

    #[test]
    fn never_is_off_and_always_is_on_regardless_of_tty() {
        assert!(!ColorChoice::Never.resolve(true));
        assert!(ColorChoice::Always.resolve(false));
    }

    #[test]
    fn auto_follows_the_terminal() {
        // NO_COLOR is not set in the test environment by default; auto then
        // tracks the terminal flag.
        if std::env::var_os("NO_COLOR").is_none() {
            assert!(ColorChoice::Auto.resolve(true));
            assert!(!ColorChoice::Auto.resolve(false));
        }
    }

    #[test]
    fn enabled_styler_wraps_text_in_sgr_codes() {
        let s = Styler::new(true);
        let painted = s.fail("FAIL");
        assert!(painted.contains("FAIL"));
        assert!(painted.contains('\u{1b}'), "expected an ANSI escape");
        assert!(painted.ends_with("\u{1b}[0m"), "expected a reset");
    }

    #[test]
    fn disabled_styler_is_a_passthrough() {
        let s = Styler::plain();
        assert_eq!(s.fail("FAIL"), "FAIL");
        assert_eq!(s.pass("PASS"), "PASS");
        assert!(!s.enabled());
    }
}
