//! Theme system for oxi-tui.
//!
//! Provides customizable color schemes, font styles, and spacing.
//! Includes built-in dark and light themes, and supports loading
//! themes from TOML or JSON files with hot-reloading.

use crate::cell::Color;
use ratatui::style::Style;
use serde::Deserialize;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Instant;

// ---------------------------------------------------------------------------
// Core theme types
// ---------------------------------------------------------------------------

/// A complete theme definition.
#[derive(Clone, Debug)]
pub struct Theme {
    /// Human-readable theme name.
    pub name: String,
    /// Color palette.
    pub colors: ColorScheme,
    /// Spacing configuration.
    pub spacing: Spacing,
}

// ---------------------------------------------------------------------------
// Color scheme
// ---------------------------------------------------------------------------

/// Semantic color palette used by components.
#[derive(Clone, Debug)]
pub struct ColorScheme {
    /// Normal text foreground color.
    pub foreground: Color,
    /// Default background color.
    pub background: Color,
    /// Primary accent color (UI elements, labels, user "You").
    pub primary: Color,
    /// Secondary color (alternative accents).
    pub secondary: Color,
    /// Error / danger color.
    pub error: Color,
    /// Warning / caution color.
    pub warning: Color,
    /// Success / confirmation color.
    pub success: Color,
    /// Muted / dimmed text (e.g. placeholders, tool headers).
    pub muted: Color,
    /// Accent highlight color.
    pub accent: Color,
    /// Border / separator color.
    pub border: Color,
    /// User message left-border accent (subtle).
    pub user_border: Color,
    /// User message background (subtle tint).
    pub user_bg: Color,
    /// Cursor foreground.
    pub cursor_fg: Color,
    /// Cursor background.
    pub cursor_bg: Color,
    /// Selection / highlight background.
    pub selection_bg: Color,
    /// Code (inline `code`) foreground color.
    pub code_fg: Color,
    /// Code (inline `code`) background color.
    pub code_bg: Color,
    /// Tool call pending background (waiting state).
    pub tool_pending_bg: Color,
    /// Tool call executing background (running state).
    pub tool_executing_bg: Color,
    /// Tool call success background (completed successfully).
    pub tool_success_bg: Color,
    /// Tool call error background (completed with error).
    pub tool_error_bg: Color,
}

impl Default for ColorScheme {
    fn default() -> Self {
        Self::dark()
    }
}

impl ColorScheme {
    /// Default dark color scheme (true black).
    pub fn dark() -> Self {
        Self {
            foreground: Color::Rgb(205, 214, 244),     // #cdd6f4
            background: Color::Rgb(0, 0, 0),           // #000000 true black
            primary: Color::Rgb(122, 162, 247),        // #7aa2f7
            secondary: Color::Rgb(158, 206, 106),      // #9ece6a
            error: Color::Rgb(247, 118, 142),          // #f7768e
            warning: Color::Rgb(224, 175, 104),        // #e0af68
            success: Color::Rgb(158, 206, 106),        // #9ece6a
            muted: Color::Rgb(80, 80, 100),            // #505064
            accent: Color::Rgb(187, 154, 247),         // #bb9af7
            border: Color::Rgb(30, 30, 30),            // #1e1e1e
            user_border: Color::Rgb(122, 162, 247),    // #7aa2f7 (matches primary)
            user_bg: Color::Rgb(18, 22, 38),           // #121626 subtle indigo tint
            cursor_fg: Color::Rgb(0, 0, 0),            // #000000
            cursor_bg: Color::Rgb(205, 214, 244),      // #cdd6f4
            selection_bg: Color::Rgb(40, 40, 60),      // #28283c
            code_fg: Color::Rgb(255, 200, 100),        // #ffc864 warm amber
            code_bg: Color::Rgb(35, 30, 20),           // #231e14 warm dark
            tool_pending_bg: Color::Rgb(18, 20, 28),   // #12141c subtle
            tool_executing_bg: Color::Rgb(28, 24, 14), // #1c1810 amber tint
            tool_success_bg: Color::Rgb(16, 26, 14),   // #101a0e green tint
            tool_error_bg: Color::Rgb(32, 16, 18),     // #201012 red tint
        }
    }

    /// Default light color scheme.
    pub fn light() -> Self {
        Self {
            foreground: Color::Rgb(76, 79, 105),   // #4c4f69
            background: Color::Rgb(239, 241, 245), // #eff1f5
            primary: Color::Rgb(30, 102, 240),     // #1e66f0
            secondary: Color::Rgb(64, 160, 43),    // #40a02b
            error: Color::Rgb(210, 15, 57),        // #d20f39
            warning: Color::Rgb(223, 142, 29),     // #df8e1d
            success: Color::Rgb(64, 160, 43),      // #40a02b
            muted: Color::Indexed(8),
            accent: Color::Rgb(136, 57, 239), // #8839ef
            border: Color::Indexed(7),
            user_border: Color::Rgb(30, 102, 240), // #1e66f0 (matches primary)
            user_bg: Color::Rgb(225, 236, 255),    // #e1ecff subtle blue tint
            cursor_fg: Color::Rgb(239, 241, 245),
            cursor_bg: Color::Rgb(76, 79, 105),
            selection_bg: Color::Rgb(204, 208, 218),
            code_fg: Color::Rgb(180, 60, 60),   // #b43c3c dark red
            code_bg: Color::Rgb(240, 240, 245), // #f0f0f5 off-white
            tool_pending_bg: Color::Rgb(235, 238, 245), // #ebeeff subtle blue tint
            tool_executing_bg: Color::Rgb(255, 248, 230), // #fff8e6 amber tint
            tool_success_bg: Color::Rgb(230, 248, 230), // #e6f8e6 green tint
            tool_error_bg: Color::Rgb(255, 230, 235), // #ffe6eb red tint
        }
    }

    /// Convert to ratatui Style with foreground and background.
    pub fn to_style(&self) -> Style {
        Style::default().fg(self.foreground).bg(self.background)
    }

    /// Convert to ratatui Style with all semantic colors.
    pub fn to_styles(&self) -> ThemeStyles {
        ThemeStyles {
            normal: Style::default().fg(self.foreground),
            primary: Style::default().fg(self.primary),
            secondary: Style::default().fg(self.secondary),
            error: Style::default().fg(self.error),
            warning: Style::default().fg(self.warning),
            success: Style::default().fg(self.success),
            muted: Style::default().fg(self.muted),
            accent: Style::default().fg(self.accent),
            border: Style::default().fg(self.border),
            cursor_fg: Style::default().fg(self.cursor_fg),
            cursor_bg: Style::default().fg(self.cursor_bg),
            selection_bg: Style::default().bg(self.selection_bg),
            user_border: Style::default().fg(self.user_border),
            user_bg: Style::default().bg(self.user_bg),
            tool_pending_bg: Style::default().bg(self.tool_pending_bg),
            tool_executing_bg: Style::default().bg(self.tool_executing_bg),
            tool_success_bg: Style::default().bg(self.tool_success_bg),
            tool_error_bg: Style::default().bg(self.tool_error_bg),
            code_fg: Style::default().fg(self.code_fg),
            code_bg: Style::default().bg(self.code_bg),
        }
    }
}

/// Pre-computed ratatui styles for all semantic colors in a ColorScheme.
///
/// Every style field is a `Style` with only the relevant property set (fg or bg),
/// so they compose correctly via `Style::patch()`.
#[derive(Debug, Clone, Copy, Default)]
pub struct ThemeStyles {
    // ── Text ──────────────────────────────────────────────────────
    /// Normal / default text style.
    pub normal: Style,
    /// Primary accent color style (UI elements, labels, user "You").
    pub primary: Style,
    /// Secondary color style.
    pub secondary: Style,
    /// Error / red style.
    pub error: Style,
    /// Warning / yellow style.
    pub warning: Style,
    /// Success / green style.
    pub success: Style,
    /// Muted / dimmed style (tool headers, borders).
    pub muted: Style,
    /// Accent / highlight style.
    pub accent: Style,

    // ── Structural ────────────────────────────────────────────────
    /// Border / separator style.
    pub border: Style,
    /// Cursor foreground style.
    pub cursor_fg: Style,
    /// Cursor background style.
    pub cursor_bg: Style,
    /// Selection background style.
    pub selection_bg: Style,

    // ── User messages ─────────────────────────────────────────────
    /// User message left-border accent (bright primary).
    pub user_border: Style,
    /// User message background (subtle tint).
    pub user_bg: Style,

    // ── Tool call states ──────────────────────────────────────────
    /// Tool call pending background (waiting state).
    pub tool_pending_bg: Style,
    /// Tool call executing background (running state).
    pub tool_executing_bg: Style,
    /// Tool call success background (completed successfully).
    pub tool_success_bg: Style,
    /// Tool call error background (completed with error).
    pub tool_error_bg: Style,

    // ── Code ──────────────────────────────────────────────────────
    /// Inline code foreground style.
    pub code_fg: Style,
    /// Inline code / code block background style.
    pub code_bg: Style,
}

// ---------------------------------------------------------------------------
// Spacing
// ---------------------------------------------------------------------------

/// Spacing/padding configuration (in character cells).
#[derive(Clone, Debug, Copy)]
pub struct Spacing {
    /// Padding around content.
    pub padding: u16,
    /// Outer margin.
    pub margin: u16,
    /// Width of borders.
    pub border_width: u16,
    /// Extra line spacing.
    pub line_spacing: u16,
}

impl Default for Spacing {
    fn default() -> Self {
        Self {
            padding: 1,
            margin: 0,
            border_width: 1,
            line_spacing: 0,
        }
    }
}

// ---------------------------------------------------------------------------
// Built-in themes
// ---------------------------------------------------------------------------

impl Theme {
    /// Built-in dark theme.
    pub fn dark() -> Self {
        Self {
            name: "dark".into(),
            colors: ColorScheme::dark(),
            spacing: Spacing::default(),
        }
    }

    /// Built-in light theme.
    pub fn light() -> Self {
        Self {
            name: "light".into(),
            colors: ColorScheme::light(),
            spacing: Spacing::default(),
        }
    }

    /// Convert theme foreground/background to ratatui Style.
    pub fn to_style(&self) -> Style {
        self.colors.to_style()
    }

    /// Get all semantic styles as ThemeStyles.
    pub fn to_styles(&self) -> ThemeStyles {
        self.colors.to_styles()
    }
}

impl Default for Theme {
    fn default() -> Self {
        Self::dark()
    }
}

// ---------------------------------------------------------------------------
// Theme loading from files (TOML / JSON)
// ---------------------------------------------------------------------------

/// Serializable representation of a theme file.
#[derive(Clone, Debug, Deserialize, Default)]
pub struct ThemeFile {
    /// Human-readable name of the theme.
    #[serde(default)]
    pub name: String,
    /// Color definitions.
    #[serde(default)]
    pub colors: ThemeFileColors,
}

/// Color overrides from a theme file.
#[derive(Clone, Debug, Deserialize, Default)]
pub struct ThemeFileColors {
    /// Foreground / text color.
    pub foreground: Option<String>,
    /// Background color.
    pub background: Option<String>,
    /// Primary accent color.
    pub primary: Option<String>,
    /// Secondary color.
    pub secondary: Option<String>,
    /// Error color.
    pub error: Option<String>,
    /// Warning color.
    pub warning: Option<String>,
    /// Success color.
    pub success: Option<String>,
    /// Muted / dimmed text color.
    pub muted: Option<String>,
    /// Accent highlight color.
    pub accent: Option<String>,
    /// Border / separator color.
    pub border: Option<String>,
    /// User message left-border accent.
    pub user_border: Option<String>,
    /// User message background (subtle tint).
    pub user_bg: Option<String>,
    /// Cursor foreground color.
    pub cursor_fg: Option<String>,
    /// Cursor background color.
    pub cursor_bg: Option<String>,
    /// Selection background color.
    pub selection_bg: Option<String>,
    /// Code (inline `code`) foreground color.
    pub code_fg: Option<String>,
    /// Code (inline `code`) background color.
    pub code_bg: Option<String>,
    /// Tool call pending background (waiting state).
    pub tool_pending_bg: Option<String>,
    /// Tool call executing background (running state).
    pub tool_executing_bg: Option<String>,
    /// Tool call success background (completed successfully).
    pub tool_success_bg: Option<String>,
    /// Tool call error background (completed with error).
    pub tool_error_bg: Option<String>,
}

impl ThemeFile {
    /// Load a theme from a TOML file.
    pub fn from_toml(path: &Path) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let theme: ThemeFile = toml::from_str(&content)?;
        Ok(theme)
    }

    /// Load a theme from a JSON file.
    pub fn from_json(path: &Path) -> anyhow::Result<Self> {
        let content = std::fs::read_to_string(path)?;
        let theme: ThemeFile = serde_json::from_str(&content)?;
        Ok(theme)
    }

    /// Load from any supported format (detected by extension).
    pub fn load(path: &Path) -> anyhow::Result<Self> {
        match path.extension().and_then(|e| e.to_str()) {
            Some("toml") => Self::from_toml(path),
            Some("json") => Self::from_json(path),
            _ => anyhow::bail!(
                "Unsupported theme file format: {:?}. Use .toml or .json",
                path.extension()
            ),
        }
    }

    /// Convert into a full Theme, using dark defaults for any missing fields.
    pub fn into_theme(self) -> Theme {
        let defaults = ColorScheme::dark();

        // Helper: parse a color string, logging a warning for invalid user-specified values.
        fn resolve(value: Option<String>, fallback: Color, field_name: &str) -> Color {
            match value.as_deref().and_then(parse_color) {
                Some(c) => c,
                None => {
                    if let Some(ref v) = value {
                        tracing::warn!(
                            "Invalid theme color for '{}': '{}' - using default",
                            field_name,
                            v
                        );
                    }
                    fallback
                }
            }
        }

        let colors = ColorScheme {
            foreground: resolve(self.colors.foreground, defaults.foreground, "foreground"),
            background: resolve(self.colors.background, defaults.background, "background"),
            primary: resolve(self.colors.primary, defaults.primary, "primary"),
            secondary: resolve(self.colors.secondary, defaults.secondary, "secondary"),
            error: resolve(self.colors.error, defaults.error, "error"),
            warning: resolve(self.colors.warning, defaults.warning, "warning"),
            success: resolve(self.colors.success, defaults.success, "success"),
            muted: resolve(self.colors.muted, defaults.muted, "muted"),
            accent: resolve(self.colors.accent, defaults.accent, "accent"),
            border: resolve(self.colors.border, defaults.border, "border"),
            user_border: resolve(self.colors.user_border, defaults.user_border, "user_border"),
            user_bg: resolve(self.colors.user_bg, defaults.user_bg, "user_bg"),
            cursor_fg: resolve(self.colors.cursor_fg, defaults.cursor_fg, "cursor_fg"),
            cursor_bg: resolve(self.colors.cursor_bg, defaults.cursor_bg, "cursor_bg"),
            selection_bg: resolve(
                self.colors.selection_bg,
                defaults.selection_bg,
                "selection_bg",
            ),
            code_fg: resolve(self.colors.code_fg, defaults.code_fg, "code_fg"),
            code_bg: resolve(self.colors.code_bg, defaults.code_bg, "code_bg"),
            tool_pending_bg: resolve(
                self.colors.tool_pending_bg,
                defaults.tool_pending_bg,
                "tool_pending_bg",
            ),
            tool_executing_bg: resolve(
                self.colors.tool_executing_bg,
                defaults.tool_executing_bg,
                "tool_executing_bg",
            ),
            tool_success_bg: resolve(
                self.colors.tool_success_bg,
                defaults.tool_success_bg,
                "tool_success_bg",
            ),
            tool_error_bg: resolve(
                self.colors.tool_error_bg,
                defaults.tool_error_bg,
                "tool_error_bg",
            ),
        };
        Theme {
            name: if self.name.is_empty() {
                "custom".into()
            } else {
                self.name
            },
            colors,
            spacing: Spacing::default(),
        }
    }
}

/// Parse a color string.
///
/// Accepted forms:
/// - Named: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`
/// - Bright named: `bright-black`, `bright-red`, ...
/// - Hex: `#rrggbb`
/// - Indexed: `i<N>` where N is 0–255
fn parse_color(s: &str) -> Option<Color> {
    let s = s.trim();
    // Hex
    if let Some(hex) = s.strip_prefix('#') {
        return parse_hex(hex);
    }
    // Indexed
    if let Some(idx_str) = s.strip_prefix('i')
        && let Ok(n) = idx_str.parse::<u8>()
    {
        return Some(Color::Indexed(n));
    }
    // Named
    match s.to_lowercase().as_str() {
        "black" => Some(Color::Black),
        "red" => Some(Color::Red),
        "green" => Some(Color::Green),
        "yellow" => Some(Color::Yellow),
        "blue" => Some(Color::Blue),
        "magenta" => Some(Color::Magenta),
        "cyan" => Some(Color::Cyan),
        "white" => Some(Color::White),
        "bright-black" | "brightblack" | "gray" | "grey" => Some(Color::Indexed(8)),
        "bright-red" | "brightred" => Some(Color::Indexed(9)),
        "bright-green" | "brightgreen" => Some(Color::Indexed(10)),
        "bright-yellow" | "brightyellow" => Some(Color::Indexed(11)),
        "bright-blue" | "brightblue" => Some(Color::Indexed(12)),
        "bright-magenta" | "brightmagenta" => Some(Color::Indexed(13)),
        "bright-cyan" | "brightcyan" => Some(Color::Indexed(14)),
        "bright-white" | "brightwhite" => Some(Color::Indexed(15)),
        "default" => Some(Color::Reset),
        _ => None,
    }
}

fn parse_hex(hex: &str) -> Option<Color> {
    match hex.len() {
        6 => {
            let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
            let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
            let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
            Some(Color::Rgb(r, g, b))
        }
        3 => {
            let r = u8::from_str_radix(&hex[0..1].repeat(2), 16).ok()?;
            let g = u8::from_str_radix(&hex[1..2].repeat(2), 16).ok()?;
            let b = u8::from_str_radix(&hex[2..3].repeat(2), 16).ok()?;
            Some(Color::Rgb(r, g, b))
        }
        _ => None,
    }
}

// ---------------------------------------------------------------------------
// Theme manager with hot-reload
// ---------------------------------------------------------------------------

/// Manages the active theme and optionally watches a theme file for changes.
pub struct ThemeManager {
    /// Currently active theme.
    theme: Arc<parking_lot::RwLock<Theme>>,
    /// Optional file being watched.
    watch_path: Option<PathBuf>,
    /// Last known modification time.
    last_modified: Option<std::time::SystemTime>,
    /// Polling interval for file changes.
    poll_interval: std::time::Duration,
    /// Instant of last poll.
    last_poll: Instant,
}

impl ThemeManager {
    /// Create a new manager with the given theme.
    pub fn new(theme: Theme) -> Self {
        Self {
            theme: Arc::new(parking_lot::RwLock::new(theme)),
            watch_path: None,
            last_modified: None,
            poll_interval: std::time::Duration::from_secs(1),
            last_poll: Instant::now(),
        }
    }

    /// Create a manager that starts with the default dark theme.
    pub fn dark() -> Self {
        Self::new(Theme::dark())
    }

    /// Create a manager that starts with the default light theme.
    pub fn light() -> Self {
        Self::new(Theme::light())
    }

    /// Start watching a theme file for changes.
    ///
    /// The file format is auto-detected from the extension (`.toml` or `.json`).
    /// On each call to [`ThemeManager::check_reload`], the file's mtime is
    /// compared to the last known value; if it changed, the theme is reloaded.
    pub fn watch_file(&mut self, path: impl Into<PathBuf>) -> anyhow::Result<()> {
        let path = path.into();
        // Immediately load the theme
        let file = ThemeFile::load(&path)?;
        let theme = file.into_theme();
        *self.theme.write() = theme;
        self.last_modified = std::fs::metadata(&path)
            .ok()
            .and_then(|m| m.modified().ok());
        self.watch_path = Some(path);
        Ok(())
    }

    /// Get a clone of the current theme.
    pub fn theme(&self) -> Theme {
        self.theme.read().clone()
    }

    /// Get a handle to the shared theme lock.
    pub fn theme_handle(&self) -> Arc<parking_lot::RwLock<Theme>> {
        Arc::clone(&self.theme)
    }

    /// Replace the active theme.
    pub fn set_theme(&self, theme: Theme) {
        *self.theme.write() = theme;
    }

    /// Set the active theme by name ("dark" or "light").
    ///
    /// Returns `true` if the name was recognized.
    pub fn set_theme_by_name(&self, name: &str) -> bool {
        let theme = match name {
            "dark" => Theme::dark(),
            "light" => Theme::light(),
            _ => return false,
        };
        self.set_theme(theme);
        true
    }

    /// Check if the watched file has changed and reload if so.
    ///
    /// Call this periodically (e.g. once per event-loop tick).
    /// Returns `true` if the theme was reloaded.
    pub fn check_reload(&mut self) -> bool {
        let path = match &self.watch_path {
            Some(p) => p.clone(),
            None => return false,
        };

        // Throttle polling
        if self.last_poll.elapsed() < self.poll_interval {
            return false;
        }
        self.last_poll = Instant::now();

        let current_mtime = match std::fs::metadata(&path)
            .ok()
            .and_then(|m| m.modified().ok())
        {
            Some(t) => t,
            None => return false,
        };

        let changed = match self.last_modified {
            Some(prev) => current_mtime > prev,
            None => true,
        };

        if changed {
            match ThemeFile::load(&path) {
                Ok(file) => {
                    let theme = file.into_theme();
                    *self.theme.write() = theme;
                    self.last_modified = Some(current_mtime);
                    tracing::info!("Theme reloaded from {:?}", path);
                    true
                }
                Err(e) => {
                    tracing::warn!("Failed to reload theme from {:?}: {}", path, e);
                    false
                }
            }
        } else {
            false
        }
    }

    /// Set the polling interval for file watching (default 1s).
    pub fn set_poll_interval(&mut self, interval: std::time::Duration) {
        self.poll_interval = interval;
    }
}

impl fmt::Display for Theme {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Theme({})", self.name)
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

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

    #[test]
    fn default_theme_is_dark() {
        let theme = Theme::default();
        assert_eq!(theme.name, "dark");
    }

    #[test]
    fn dark_theme_has_light_foreground() {
        let theme = Theme::dark();
        // foreground should be a light color
        match theme.colors.foreground {
            Color::Rgb(r, _, _) => assert!(r > 200, "dark theme foreground should be light"),
            _ => panic!("expected Rgb foreground"),
        }
    }

    #[test]
    fn light_theme_has_dark_foreground() {
        let theme = Theme::light();
        match theme.colors.foreground {
            Color::Rgb(r, _, _) => assert!(r < 150, "light theme foreground should be dark"),
            _ => panic!("expected Rgb foreground"),
        }
    }

    #[test]
    fn parse_hex_colors() {
        assert_eq!(parse_color("#ff8800"), Some(Color::Rgb(255, 136, 0)));
        assert_eq!(parse_color("#f80"), Some(Color::Rgb(255, 136, 0)));
    }

    #[test]
    fn parse_named_colors() {
        assert_eq!(parse_color("red"), Some(Color::Red));
        assert_eq!(parse_color("bright-black"), Some(Color::Indexed(8)));
        assert_eq!(parse_color("default"), Some(Color::Reset));
    }

    #[test]
    fn parse_indexed_color() {
        assert_eq!(parse_color("i42"), Some(Color::Indexed(42)));
    }

    #[test]
    fn theme_manager_set_by_name() {
        let mgr = ThemeManager::dark();
        assert!(mgr.set_theme_by_name("light"));
        assert_eq!(mgr.theme().name, "light");
        assert!(!mgr.set_theme_by_name("nonexistent"));
        assert_eq!(mgr.theme().name, "light");
    }

    #[test]
    fn theme_file_from_json() {
        let json = r##"{"name":"test","colors":{"foreground":"#ffffff","background":"#000000"}}"##;
        let file: ThemeFile = serde_json::from_str(json).unwrap();
        let theme = file.into_theme();
        assert_eq!(theme.name, "test");
        assert_eq!(theme.colors.foreground, Color::Rgb(255, 255, 255));
        assert_eq!(theme.colors.background, Color::Rgb(0, 0, 0));
    }

    #[test]
    fn theme_file_roundtrip() {
        let dir = std::env::temp_dir().join("oxi-tui-theme-test");
        std::fs::create_dir_all(&dir).unwrap();

        let json_path = dir.join("test_theme.json");
        std::fs::write(
            &json_path,
            r##"{"name":"mytheme","colors":{"primary":"#ff0000"}}"##,
        )
        .unwrap();
        let file = ThemeFile::load(&json_path).unwrap();
        let theme = file.into_theme();
        assert_eq!(theme.name, "mytheme");
        assert_eq!(theme.colors.primary, Color::Rgb(255, 0, 0));
        // Other fields get dark defaults
        assert!(matches!(theme.colors.foreground, Color::Rgb(_, _, _)));

        std::fs::remove_dir_all(&dir).ok();
    }
}
