sdd-layer 0.19.0

Spec-Driven Development CLI and agent harness
//! Componente Banner do TUI (T-01, T-03): exibe o logo ASCII "SDD" com gradiente
//! de cor linha a linha (truecolor) ou compacto em terminais estreitos.
//! Puro, sem estado externo — testável com `TestBackend`.

#![allow(dead_code)]

use ratatui::layout::{Alignment, Rect};
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span, Text};
use ratatui::widgets::Paragraph;
use ratatui::Frame;

use super::super::theme::{self, Theme};

// ---------------------------------------------------------------------------
// Arte ASCII do logo "SDD" — 5 linhas, largura máxima ~42 colunas
// ---------------------------------------------------------------------------

/// Linhas do logo ASCII "SDD" desenhadas à mão.
/// Largura efetiva: 42 colunas — cabe confortavelmente em terminais de 48+.
const LOGO_LINES: [&str; 5] = [
    " ███████╗██████╗ ██████╗ ",
    " ██╔════╝██╔══██╗██╔══██╗",
    " ███████╗██║  ██║██║  ██║",
    " ╚════██║██║  ██║██║  ██║",
    " ███████║██████╔╝██████╔╝",
];

// ---------------------------------------------------------------------------
// render
// ---------------------------------------------------------------------------

/// Renderiza o banner do `sdd` dentro de `area`.
///
/// - `area.width >= 48`: logo ASCII multi-linha com gradiente aplicado linha a
///   linha (uma cor por linha, interpolada entre `theme.primary` e
///   `theme.section_header`). Abaixo do logo: `version` (text_muted) e
///   `model_line` opcional (text_strong), centralizados.
/// - `area.width < 48`: versão compacta — uma única linha `" SDD · <version>"`.
///
/// Nunca causa panic. Não depende de estado externo.
pub fn render(frame: &mut Frame, area: Rect, version: &str, model_line: &str, theme: &Theme) {
    if area.height == 0 {
        return;
    }

    let widget = if area.width >= 48 {
        build_wide(version, model_line, theme)
    } else {
        build_compact(version, theme)
    };

    frame.render_widget(widget, area);
}

// ---------------------------------------------------------------------------
// Helpers privados
// ---------------------------------------------------------------------------

/// Constrói o `Paragraph` largo (logo + metadados).
fn build_wide<'a>(version: &str, model_line: &str, theme: &Theme) -> Paragraph<'a> {
    let n_logo_lines = LOGO_LINES.len();

    // Gera `n_logo_lines` cores interpolando entre primary e section_header.
    let colors = theme::gradient(&[theme.primary, theme.section_header], n_logo_lines);

    // Uma linha de logo por cor do gradiente.
    let mut lines: Vec<Line<'a>> = LOGO_LINES
        .iter()
        .enumerate()
        .map(|(i, &texto)| {
            // Usa a cor do gradiente quando disponível; fallback para primary.
            let cor: Color = colors.get(i).copied().unwrap_or(theme.primary);
            Line::from(Span::styled(
                texto,
                Style::default().fg(cor).add_modifier(Modifier::BOLD),
            ))
        })
        .collect();

    // Linha em branco de separação.
    lines.push(Line::from(""));

    let mut meta = vec![Span::styled(
        version.to_owned(),
        Style::default().fg(theme.text_muted),
    )];
    if !model_line.is_empty() {
        meta.push(Span::styled("  ·  ", Style::default().fg(theme.text_muted)));
        meta.push(Span::styled(
            model_line.to_owned(),
            Style::default()
                .fg(theme.text_strong)
                .add_modifier(Modifier::BOLD),
        ));
    }
    lines.push(Line::from(meta));

    Paragraph::new(Text::from(lines)).alignment(Alignment::Center)
}

/// Constrói o `Paragraph` compacto (terminal estreito).
fn build_compact<'a>(version: &str, theme: &Theme) -> Paragraph<'a> {
    let line = Line::from(vec![
        Span::styled(
            " SDD",
            Style::default()
                .fg(theme.primary)
                .add_modifier(Modifier::BOLD),
        ),
        Span::styled(
            format!(" · {version}"),
            Style::default().fg(theme.text_muted),
        ),
    ]);
    Paragraph::new(Text::from(vec![line]))
}

// ---------------------------------------------------------------------------
// Testes
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use ratatui::backend::TestBackend;
    use ratatui::layout::Rect;
    use ratatui::Terminal;

    use crate::tui::theme::{self as ttheme, ColorDepth};

    fn make_terminal(cols: u16, rows: u16) -> Terminal<TestBackend> {
        Terminal::new(TestBackend::new(cols, rows)).unwrap()
    }

    /// Smoke test: render largo (80 colunas) não causa panic.
    #[test]
    fn render_wide_sem_panic() {
        let mut terminal = make_terminal(80, 20);
        let theme = ttheme::theme(ColorDepth::TrueColor);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 80, 20);
                render(f, area, "v0.4.3", "claude-sonnet-4-5", &theme);
            })
            .unwrap();
    }

    /// Smoke test: render compacto (40 colunas) não causa panic.
    #[test]
    fn render_compact_sem_panic() {
        let mut terminal = make_terminal(40, 5);
        let theme = ttheme::theme(ColorDepth::Ansi16);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 40, 5);
                render(f, area, "v0.4.3", "claude-sonnet-4-5", &theme);
            })
            .unwrap();
    }

    /// Smoke test: render com área zero não causa panic.
    #[test]
    fn render_area_zero_sem_panic() {
        let mut terminal = make_terminal(10, 1);
        let theme = ttheme::theme(ColorDepth::Ansi256);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 10, 0);
                render(f, area, "v0.0.0", "", &theme);
            })
            .unwrap();
    }

    /// Smoke test: render no limite exato de 48 colunas (modo largo).
    #[test]
    fn render_boundary_48_sem_panic() {
        let mut terminal = make_terminal(48, 12);
        let theme = ttheme::theme(ColorDepth::TrueColor);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 48, 12);
                render(f, area, "v0.4.3", "gpt-4o", &theme);
            })
            .unwrap();
    }

    /// Smoke test: render em 47 colunas (modo compacto).
    #[test]
    fn render_boundary_47_sem_panic() {
        let mut terminal = make_terminal(47, 3);
        let theme = ttheme::theme(ColorDepth::TrueColor);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 47, 3);
                render(f, area, "v0.4.3", "gpt-4o", &theme);
            })
            .unwrap();
    }

    /// Garante que `build_wide` produz pelo menos tantas linhas quanto o logo.
    #[test]
    fn build_wide_tem_linhas_suficientes() {
        let theme = ttheme::theme(ColorDepth::TrueColor);
        // build_wide = linhas logo + linha em branco + linha de metadados
        let para = build_wide("v0.4.3", "modelo-x", &theme);
        // Paragraph não expõe linhas diretamente; validamos via render smoke.
        // Aqui apenas garantimos que a construção não causa panic.
        let _ = para;
    }

    /// Garante que o fallback Ansi256 no tema não quebra o render largo.
    #[test]
    fn render_wide_ansi256_sem_panic() {
        let mut terminal = make_terminal(80, 20);
        let theme = ttheme::theme(ColorDepth::Ansi256);
        terminal
            .draw(|f| {
                let area = Rect::new(0, 0, 80, 20);
                render(f, area, "v0.4.3", "claude-sonnet-4-5", &theme);
            })
            .unwrap();
    }
}