//! Theme contracts (port of `src/ui/theme-types.ts`).
//!
//! TS expresses each styling slot as a `(text: string) => string` function. In
//! Rust those become boxed `Fn` closures (`StyleFn`). These are pure contract
//! shapes with no runtime — concrete themes are supplied by the render layer.

/// A styling function: takes text, returns the same text wrapped in ANSI codes.
/// Mirrors the TS `(text: string) => string` shape.
pub type StyleFn = Box<dyn Fn(&str) -> String + Send + Sync>;

/// A selection-aware styling function: `(text, selected) -> styled`.
/// Mirrors the TS `(text: string, selected: boolean) => string` shape.
pub type SelectableStyleFn = Box<dyn Fn(&str, bool) -> String + Send + Sync>;

/// Optional syntax-highlight function: `(code, lang?) -> highlighted lines`.
pub type HighlightFn = Box<dyn Fn(&str, Option<&str>) -> Vec<String> + Send + Sync>;

/// Styling for a fuzzy-filtered select list (≈ TS `SelectListTheme`).
pub struct SelectListTheme {
    pub selected_text: StyleFn,
    pub selected_prefix: StyleFn,
    pub no_match: StyleFn,
    pub description: StyleFn,
    pub scroll_info: StyleFn,
}

/// Styling for the inline editor (≈ TS `EditorTheme`).
pub struct EditorTheme {
    pub select_list: SelectListTheme,
    pub border_color: StyleFn,
}

/// Styling for markdown rendering (≈ TS `MarkdownTheme`).
///
/// `highlight_code` and `code_block_indent` are optional in TS, so they are
/// `Option` here. `highlight_code` takes `(code, lang?)` and returns the
/// highlighted lines.
pub struct MarkdownTheme {
    pub heading: StyleFn,
    pub bold: StyleFn,
    pub italic: StyleFn,
    pub strikethrough: StyleFn,
    pub underline: StyleFn,
    pub link: StyleFn,
    pub link_url: StyleFn,
    pub list_bullet: StyleFn,
    pub quote: StyleFn,
    pub quote_border: StyleFn,
    pub code: StyleFn,
    pub code_block: StyleFn,
    pub code_block_border: StyleFn,
    pub hr: StyleFn,
    /// `(code, lang?) -> lines`.
    pub highlight_code: Option<HighlightFn>,
    pub code_block_indent: Option<String>,
}

/// Styling for a key/value settings list (≈ TS `SettingsListTheme`).
///
/// `label` / `value` receive `(text, selected)`; the rest take just text.
/// `cursor` is a plain pre-rendered string, not a function.
pub struct SettingsListTheme {
    pub cursor: String,
    pub label: SelectableStyleFn,
    pub value: SelectableStyleFn,
    pub hint: StyleFn,
    pub description: StyleFn,
}

#[cfg(test)]
mod tests {
    //! These mirror the TS `theme-types.ts` contract surface. The TS originals
    //! are pure `interface`s with no runtime, so the testable behavior is: each
    //! styling slot is a callable that transforms text, the structs carry the
    //! same field set, and the optional `MarkdownTheme` slots really are
    //! optional (TS `?`). The painters here use plain wrapping (e.g. `[..]`)
    //! standing in for the ANSI codes the real render-layer themes inject.
    use super::*;

    /// A trivial painter that wraps text in brackets, standing in for an
    /// ANSI-injecting `(text) => string` chalk closure from the TS source.
    fn brackets(tag: &'static str) -> StyleFn {
        Box::new(move |text: &str| format!("[{tag}]{text}[/{tag}]"))
    }

    #[test]
    fn select_list_theme_painters_transform_text() {
        let theme = SelectListTheme {
            selected_text: brackets("sel"),
            selected_prefix: brackets("pfx"),
            no_match: brackets("nm"),
            description: brackets("desc"),
            scroll_info: brackets("scroll"),
        };
        assert_eq!((theme.selected_text)("hi"), "[sel]hi[/sel]");
        assert_eq!((theme.selected_prefix)(">"), "[pfx]>[/pfx]");
        assert_eq!((theme.no_match)("none"), "[nm]none[/nm]");
        assert_eq!((theme.description)("d"), "[desc]d[/desc]");
        assert_eq!((theme.scroll_info)("1/9"), "[scroll]1/9[/scroll]");
    }

    #[test]
    fn editor_theme_embeds_select_list_and_border() {
        let theme = EditorTheme {
            select_list: SelectListTheme {
                selected_text: brackets("sel"),
                selected_prefix: brackets("pfx"),
                no_match: brackets("nm"),
                description: brackets("desc"),
                scroll_info: brackets("scroll"),
            },
            border_color: brackets("border"),
        };
        // The nested select-list is reachable and live.
        assert_eq!((theme.select_list.selected_text)("x"), "[sel]x[/sel]");
        assert_eq!((theme.border_color)("|"), "[border]|[/border]");
    }

    /// Build a fully-populated `MarkdownTheme` for the field-coverage tests.
    fn full_markdown_theme() -> MarkdownTheme {
        MarkdownTheme {
            heading: brackets("h"),
            bold: brackets("b"),
            italic: brackets("i"),
            strikethrough: brackets("s"),
            underline: brackets("u"),
            link: brackets("link"),
            link_url: brackets("url"),
            list_bullet: brackets("li"),
            quote: brackets("q"),
            quote_border: brackets("qb"),
            code: brackets("c"),
            code_block: brackets("cb"),
            code_block_border: brackets("cbb"),
            hr: brackets("hr"),
            highlight_code: None,
            code_block_indent: None,
        }
    }

    #[test]
    fn markdown_theme_has_all_required_painters() {
        let t = full_markdown_theme();
        assert_eq!((t.heading)("H"), "[h]H[/h]");
        assert_eq!((t.bold)("B"), "[b]B[/b]");
        assert_eq!((t.italic)("I"), "[i]I[/i]");
        assert_eq!((t.strikethrough)("S"), "[s]S[/s]");
        assert_eq!((t.underline)("U"), "[u]U[/u]");
        assert_eq!((t.link)("L"), "[link]L[/link]");
        assert_eq!((t.link_url)("http://x"), "[url]http://x[/url]");
        assert_eq!((t.list_bullet)("-"), "[li]-[/li]");
        assert_eq!((t.quote)("Q"), "[q]Q[/q]");
        assert_eq!((t.quote_border)("|"), "[qb]|[/qb]");
        assert_eq!((t.code)("x"), "[c]x[/c]");
        assert_eq!((t.code_block)("y"), "[cb]y[/cb]");
        assert_eq!((t.code_block_border)("="), "[cbb]=[/cbb]");
        assert_eq!((t.hr)("---"), "[hr]---[/hr]");
    }

    #[test]
    fn markdown_theme_optionals_default_to_none() {
        // Mirrors TS `highlightCode?` / `codeBlockIndent?` being absent.
        let t = full_markdown_theme();
        assert!(t.highlight_code.is_none());
        assert!(t.code_block_indent.is_none());
    }

    #[test]
    fn markdown_theme_highlight_code_when_present_returns_lines() {
        let mut t = full_markdown_theme();
        t.code_block_indent = Some("  ".to_string());
        t.highlight_code = Some(Box::new(|code: &str, lang: Option<&str>| {
            let prefix = lang.unwrap_or("txt");
            code.lines().map(|l| format!("{prefix}:{l}")).collect()
        }));
        let hl = t.highlight_code.as_ref().unwrap();
        assert_eq!(hl("a\nb", Some("rs")), vec!["rs:a", "rs:b"]);
        // lang omitted -> the closure's own fallback ("txt"), proving `Option<&str>`.
        assert_eq!(hl("x", None), vec!["txt:x"]);
        assert_eq!(t.code_block_indent.as_deref(), Some("  "));
    }

    #[test]
    fn settings_list_theme_cursor_is_plain_string_and_painters_take_selected() {
        let theme = SettingsListTheme {
            cursor: "> ".to_string(),
            label: Box::new(|text: &str, selected: bool| {
                if selected {
                    format!("*{text}*")
                } else {
                    text.to_string()
                }
            }),
            value: Box::new(|text: &str, selected: bool| {
                format!("{text}{}", if selected { "!" } else { "" })
            }),
            hint: brackets("hint"),
            description: brackets("desc"),
        };
        // `cursor` is a pre-rendered string, not a closure (matches TS).
        assert_eq!(theme.cursor, "> ");
        // Selection-aware painters branch on the bool.
        assert_eq!((theme.label)("Name", true), "*Name*");
        assert_eq!((theme.label)("Name", false), "Name");
        assert_eq!((theme.value)("on", true), "on!");
        assert_eq!((theme.value)("on", false), "on");
        assert_eq!((theme.hint)("?"), "[hint]?[/hint]");
        assert_eq!((theme.description)("d"), "[desc]d[/desc]");
    }
}
