use crate::addons::AddonSettings;
use crate::downloads::DownloadStore;
use crate::plugins::PluginRegistry;
use crate::privacy::{SearchProvider, Shields};
use crate::session::SessionStore;
use anyhow::{Context, bail};
use chrono::{DateTime, Utc};
use directories::ProjectDirs;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
use url::Url;

pub const DEFAULT_MAX_HISTORY: usize = 500;
pub const DEFAULT_MAX_BOOKMARKS: usize = 1_000;
pub const MAX_PROFILE_FILE_BYTES: u64 = 4 * 1024 * 1024;

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum BrowserTheme {
    Professional,
    Nebula,
    Ember,
    Glacier,
    #[default]
    Paper,
    DarkPaper,
    Cupertino,
    Glass,
    AgentInspect,
    Ocean,
    Forest,
    Rose,
    HighContrastLight,
    HighContrastDark,
    PrivateBrowsing,
    Custom,
}

impl BrowserTheme {
    pub const ALL: [Self; 16] = [
        Self::Professional,
        Self::Paper,
        Self::DarkPaper,
        Self::Cupertino,
        Self::Glass,
        Self::AgentInspect,
        Self::Nebula,
        Self::Ember,
        Self::Glacier,
        Self::Ocean,
        Self::Forest,
        Self::Rose,
        Self::HighContrastLight,
        Self::HighContrastDark,
        Self::PrivateBrowsing,
        Self::Custom,
    ];

    pub fn id(&self) -> &'static str {
        match self {
            Self::Professional => "professional",
            Self::Nebula => "nebula",
            Self::Ember => "ember",
            Self::Glacier => "glacier",
            Self::Paper => "paper",
            Self::DarkPaper => "dark-paper",
            Self::Cupertino => "cupertino",
            Self::Glass => "glass",
            Self::AgentInspect => "agent-inspect",
            Self::Ocean => "ocean",
            Self::Forest => "forest",
            Self::Rose => "rose",
            Self::HighContrastLight => "high-contrast-light",
            Self::HighContrastDark => "high-contrast-dark",
            Self::PrivateBrowsing => "private-browsing",
            Self::Custom => "custom",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::Professional => "Browser Paper",
            Self::Nebula => "Minimal Paper",
            Self::Ember => "Graphite Paper",
            Self::Glacier => "Edge Paper",
            Self::Paper => "Paper",
            Self::DarkPaper => "Dark Paper",
            Self::Cupertino => "macOS Paper",
            Self::Glass => "Glass",
            Self::AgentInspect => "AI Inspect",
            Self::Ocean => "Brave Blue",
            Self::Forest => "Vivaldi Green",
            Self::Rose => "Ubuntu Rose",
            Self::HighContrastLight => "High Contrast Light",
            Self::HighContrastDark => "High Contrast Dark",
            Self::PrivateBrowsing => "Private Paper",
            Self::Custom => "Custom",
        }
    }

    pub fn css_class(&self) -> &'static str {
        match self {
            Self::Professional => "theme-professional",
            Self::Nebula => "theme-nebula",
            Self::Ember => "theme-ember",
            Self::Glacier => "theme-glacier",
            Self::Paper => "theme-paper",
            Self::DarkPaper => "theme-dark-paper",
            Self::Cupertino => "theme-cupertino",
            Self::Glass => "theme-glass",
            Self::AgentInspect => "theme-agent-inspect",
            Self::Ocean => "theme-ocean",
            Self::Forest => "theme-forest",
            Self::Rose => "theme-rose",
            Self::HighContrastLight => "theme-high-contrast-light",
            Self::HighContrastDark => "theme-high-contrast-dark",
            Self::PrivateBrowsing => "theme-private-browsing",
            Self::Custom => "theme-custom",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|theme| theme.id() == id)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum WebAccelerationPolicy {
    OnDemand,
    #[default]
    Always,
    Never,
}

impl WebAccelerationPolicy {
    pub const ALL: [Self; 2] = [Self::Always, Self::Never];

    pub fn id(&self) -> &'static str {
        match self {
            Self::OnDemand => "on-demand",
            Self::Always => "always",
            Self::Never => "never",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::OnDemand => "On demand",
            Self::Always => "Always",
            Self::Never => "Never",
        }
    }

    pub fn description(&self) -> &'static str {
        match self {
            Self::OnDemand => {
                "Legacy auto mode; Cephas upgrades this to Always for browser-like speed."
            }
            Self::Always => {
                "Prefer GPU compositing, WebGL, accelerated canvas, and runtime WebGPU when available."
            }
            Self::Never => "Disable GPU acceleration for maximum compatibility.",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|policy| policy.id() == id)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MediaPlaybackPolicy {
    #[default]
    Balanced,
    Full,
    Strict,
}

impl MediaPlaybackPolicy {
    pub const ALL: [Self; 3] = [Self::Balanced, Self::Full, Self::Strict];

    pub fn id(&self) -> &'static str {
        match self {
            Self::Balanced => "balanced",
            Self::Full => "full",
            Self::Strict => "strict",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::Balanced => "Balanced media",
            Self::Full => "Full media",
            Self::Strict => "Strict media",
        }
    }

    pub fn description(&self) -> &'static str {
        match self {
            Self::Balanced => {
                "Enable modern playback, MediaSource, fullscreen, inline video, and encrypted media without camera or microphone capture."
            }
            Self::Full => {
                "Enable the complete WebKit media surface, including camera and microphone capture prompts for sites that request them."
            }
            Self::Strict => {
                "Keep playback support on, but require user gestures and keep capture devices disabled."
            }
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|policy| policy.id() == id)
    }

    pub fn allows_capture(&self) -> bool {
        matches!(self, Self::Full)
    }

    pub fn requires_user_gesture(&self) -> bool {
        matches!(self, Self::Strict)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum StartupBehavior {
    #[default]
    NewTabPage,
    ContinuePreviousSession,
}

impl StartupBehavior {
    pub const ALL: [Self; 2] = [Self::NewTabPage, Self::ContinuePreviousSession];

    pub fn id(&self) -> &'static str {
        match self {
            Self::NewTabPage => "new-tab-page",
            Self::ContinuePreviousSession => "continue-previous-session",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::NewTabPage => "Open the New Tab page",
            Self::ContinuePreviousSession => "Continue where you left off",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|behavior| behavior.id() == id)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CustomTheme {
    pub name: String,
    pub chrome: String,
    #[serde(default = "default_custom_page")]
    pub page: String,
    pub surface: String,
    pub text: String,
    #[serde(default = "default_custom_muted")]
    pub muted: String,
    pub accent: String,
    #[serde(default)]
    pub radius: CustomThemeRadius,
    #[serde(default)]
    pub density: CustomThemeDensity,
}

impl Default for CustomTheme {
    fn default() -> Self {
        Self {
            name: "Custom".to_string(),
            chrome: "#202124".to_string(),
            page: default_custom_page(),
            surface: "#2b2d33".to_string(),
            text: "#f8fafc".to_string(),
            muted: default_custom_muted(),
            accent: "#7aa2f7".to_string(),
            radius: CustomThemeRadius::default(),
            density: CustomThemeDensity::default(),
        }
    }
}

impl CustomTheme {
    pub fn sanitized(&self) -> Self {
        let default = Self::default();
        Self {
            name: sanitize_theme_name(&self.name),
            chrome: sanitize_hex_color(&self.chrome).unwrap_or(default.chrome),
            page: sanitize_hex_color(&self.page).unwrap_or(default.page),
            surface: sanitize_hex_color(&self.surface).unwrap_or(default.surface),
            text: sanitize_hex_color(&self.text).unwrap_or(default.text),
            muted: sanitize_hex_color(&self.muted).unwrap_or(default.muted),
            accent: sanitize_hex_color(&self.accent).unwrap_or(default.accent),
            radius: self.radius,
            density: self.density,
        }
    }
}

fn default_custom_page() -> String {
    "#151821".to_string()
}

fn default_custom_muted() -> String {
    "#a8b3c7".to_string()
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CustomThemeRadius {
    Square,
    Soft,
    #[default]
    Rounded,
    Pill,
}

impl CustomThemeRadius {
    pub const ALL: [Self; 4] = [Self::Square, Self::Soft, Self::Rounded, Self::Pill];

    pub fn id(&self) -> &'static str {
        match self {
            Self::Square => "square",
            Self::Soft => "soft",
            Self::Rounded => "rounded",
            Self::Pill => "pill",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::Square => "Square",
            Self::Soft => "Soft",
            Self::Rounded => "Rounded",
            Self::Pill => "Pill",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|radius| radius.id() == id)
    }
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CustomThemeDensity {
    Compact,
    #[default]
    Comfortable,
    Spacious,
}

impl CustomThemeDensity {
    pub const ALL: [Self; 3] = [Self::Compact, Self::Comfortable, Self::Spacious];

    pub fn id(&self) -> &'static str {
        match self {
            Self::Compact => "compact",
            Self::Comfortable => "comfortable",
            Self::Spacious => "spacious",
        }
    }

    pub fn label(&self) -> &'static str {
        match self {
            Self::Compact => "Compact",
            Self::Comfortable => "Comfortable",
            Self::Spacious => "Spacious",
        }
    }

    pub fn from_id(id: &str) -> Option<Self> {
        Self::ALL.into_iter().find(|density| density.id() == id)
    }
}

fn sanitize_theme_name(name: &str) -> String {
    let name = name.trim();
    if name.is_empty() {
        return "Custom".to_string();
    }
    name.chars().take(32).collect()
}

pub fn sanitize_hex_color(value: &str) -> Option<String> {
    let value = value.trim();
    let value = value.strip_prefix('#').unwrap_or(value);
    if value.len() == 6 && value.chars().all(|ch| ch.is_ascii_hexdigit()) {
        Some(format!("#{value}").to_ascii_lowercase())
    } else {
        None
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bookmark {
    pub title: String,
    pub url: Url,
    pub created_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistoryEntry {
    pub title: String,
    pub url: Url,
    pub visited_at: DateTime<Utc>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Profile {
    #[serde(default = "default_home_url")]
    pub home: Url,
    #[serde(default)]
    pub theme: BrowserTheme,
    #[serde(default)]
    pub custom_theme: CustomTheme,
    #[serde(default)]
    pub addons: AddonSettings,
    #[serde(default)]
    pub plugins: PluginRegistry,
    #[serde(default)]
    pub ai_browsing_enabled: bool,
    #[serde(default)]
    pub web_acceleration: WebAccelerationPolicy,
    #[serde(default)]
    pub media_playback: MediaPlaybackPolicy,
    #[serde(default)]
    pub startup_behavior: StartupBehavior,
    #[serde(default)]
    pub search_provider: SearchProvider,
    #[serde(default)]
    pub shields: Shields,
    #[serde(default)]
    pub bookmarks: Vec<Bookmark>,
    #[serde(default)]
    pub history: Vec<HistoryEntry>,
    #[serde(default = "default_max_bookmarks")]
    pub max_bookmarks: usize,
    #[serde(default = "default_max_history")]
    pub max_history: usize,
}

impl Default for Profile {
    fn default() -> Self {
        Self {
            home: default_home_url(),
            theme: BrowserTheme::default(),
            custom_theme: CustomTheme::default(),
            addons: AddonSettings::default(),
            plugins: PluginRegistry::default(),
            ai_browsing_enabled: false,
            web_acceleration: WebAccelerationPolicy::default(),
            media_playback: MediaPlaybackPolicy::default(),
            startup_behavior: StartupBehavior::default(),
            search_provider: SearchProvider::default(),
            shields: Shields::default(),
            bookmarks: Vec::new(),
            history: Vec::new(),
            max_bookmarks: DEFAULT_MAX_BOOKMARKS,
            max_history: DEFAULT_MAX_HISTORY,
        }
    }
}

impl Profile {
    pub fn enforce_limits(&mut self) {
        self.custom_theme = self.custom_theme.sanitized();
        if self.web_acceleration == WebAccelerationPolicy::OnDemand {
            self.web_acceleration = WebAccelerationPolicy::Always;
        }
        if !matches!(self.home.scheme(), "http" | "https") {
            self.home = default_home_url();
        }
        self.shields.enforce_limits();
        self.plugins.enforce_limits();

        self.max_history = self.max_history.clamp(1, DEFAULT_MAX_HISTORY);
        self.max_bookmarks = self.max_bookmarks.clamp(1, DEFAULT_MAX_BOOKMARKS);

        let max_history = self.max_history;
        if self.history.len() > max_history {
            let overflow = self.history.len() - max_history;
            self.history.drain(0..overflow);
        }

        let max_bookmarks = self.max_bookmarks;
        if self.bookmarks.len() > max_bookmarks {
            let overflow = self.bookmarks.len() - max_bookmarks;
            self.bookmarks.drain(0..overflow);
        }
    }

    pub fn record_visit(&mut self, title: impl Into<String>, url: Url) {
        self.history.push(HistoryEntry {
            title: title.into(),
            url,
            visited_at: Utc::now(),
        });
        self.enforce_limits();
    }

    pub fn add_bookmark(&mut self, title: impl Into<String>, url: Url) -> bool {
        if self.bookmarks.iter().any(|bookmark| bookmark.url == url) {
            return false;
        }

        if self.bookmarks.len() >= self.max_bookmarks.max(1) {
            return false;
        }

        self.bookmarks.push(Bookmark {
            title: title.into(),
            url,
            created_at: Utc::now(),
        });
        true
    }

    pub fn forget_history_for_host(&mut self, host: &str) -> usize {
        let before = self.history.len();
        self.history
            .retain(|entry| entry.url.host_str() != Some(host));
        before - self.history.len()
    }
}

fn default_home_url() -> Url {
    Url::parse("https://www.google.com/").expect("valid default home URL")
}

fn default_max_bookmarks() -> usize {
    DEFAULT_MAX_BOOKMARKS
}

fn default_max_history() -> usize {
    DEFAULT_MAX_HISTORY
}

#[derive(Debug, Clone)]
pub struct ProfileStore {
    path: PathBuf,
}

impl ProfileStore {
    pub fn new(path: Option<PathBuf>) -> anyhow::Result<Self> {
        let path = match path {
            Some(path) => path,
            None => default_profile_path()?,
        };
        Ok(Self { path })
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn session_store(&self) -> SessionStore {
        SessionStore::new(self.sibling_path("session.json"))
    }

    pub fn download_store(&self) -> DownloadStore {
        DownloadStore::new(self.sibling_path("downloads.json"))
    }

    pub fn load(&self) -> anyhow::Result<Profile> {
        if !self.path.exists() {
            return Ok(Profile::default());
        }

        let data = read_bounded_to_string(&self.path, MAX_PROFILE_FILE_BYTES)?;
        let mut profile: Profile = serde_json::from_str(&data)
            .with_context(|| format!("failed to parse profile {}", self.path.display()))?;
        profile.enforce_limits();
        Ok(profile)
    }

    pub fn save(&self, profile: &Profile) -> anyhow::Result<()> {
        if let Some(parent) = self.path.parent() {
            fs::create_dir_all(parent).with_context(|| {
                format!("failed to create profile directory {}", parent.display())
            })?;
        }

        let mut profile = profile.clone();
        profile.enforce_limits();
        let tmp = self.path.with_extension("tmp");
        fs::write(&tmp, serde_json::to_vec(&profile)?)
            .with_context(|| format!("failed to write profile temp file {}", tmp.display()))?;
        fs::rename(&tmp, &self.path)
            .with_context(|| format!("failed to replace profile {}", self.path.display()))
    }

    fn sibling_path(&self, file_name: &str) -> PathBuf {
        self.path
            .parent()
            .map(|parent| parent.join(file_name))
            .unwrap_or_else(|| PathBuf::from(file_name))
    }
}

fn read_bounded_to_string(path: &Path, max_bytes: u64) -> anyhow::Result<String> {
    let size = fs::metadata(path)
        .with_context(|| format!("failed to inspect profile {}", path.display()))?
        .len();
    if size > max_bytes {
        bail!(
            "profile file {} is too large: {size} bytes exceeds {max_bytes}",
            path.display()
        );
    }
    fs::read_to_string(path).with_context(|| format!("failed to read profile {}", path.display()))
}

fn default_profile_path() -> anyhow::Result<PathBuf> {
    let dirs = ProjectDirs::from("dev", "Cephas", "Cephas")
        .context("could not determine platform profile directory")?;
    Ok(dirs.data_local_dir().join("profile.json"))
}

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

    #[test]
    fn profile_round_trips() {
        let dir = tempfile::tempdir().unwrap();
        let store = ProfileStore::new(Some(dir.path().join("profile.json"))).unwrap();
        let mut profile = Profile {
            ai_browsing_enabled: true,
            ..Profile::default()
        };
        profile.add_bookmark("Example", Url::parse("https://example.com/").unwrap());
        store.save(&profile).unwrap();

        let loaded = store.load().unwrap();
        assert!(loaded.ai_browsing_enabled);
        assert_eq!(loaded.bookmarks.len(), 1);
        assert_eq!(loaded.bookmarks[0].title, "Example");
    }

    #[test]
    fn profile_load_rejects_oversized_file_before_parse() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("profile.json");
        fs::write(&path, vec![b' '; (MAX_PROFILE_FILE_BYTES + 1) as usize]).unwrap();
        let store = ProfileStore::new(Some(path)).unwrap();

        assert!(store.load().is_err());
    }

    #[test]
    fn profile_loads_when_new_fields_are_missing() {
        let mut value = serde_json::to_value(Profile::default()).unwrap();
        let object = value.as_object_mut().unwrap();
        object.remove("home");
        object.remove("theme");
        object.remove("custom_theme");
        object.remove("addons");
        object.remove("plugins");
        object.remove("ai_browsing_enabled");
        object.remove("web_acceleration");
        object.remove("media_playback");
        object.remove("startup_behavior");
        object.remove("search_provider");
        object.remove("shields");
        object.remove("bookmarks");
        object.remove("history");
        object.remove("max_bookmarks");
        object.remove("max_history");

        let loaded: Profile = serde_json::from_value(value).unwrap();
        assert_eq!(loaded.home, default_home_url());
        assert_eq!(loaded.theme, BrowserTheme::Paper);
        assert!(loaded.bookmarks.is_empty());
        assert!(loaded.history.is_empty());
        assert_eq!(loaded.max_bookmarks, DEFAULT_MAX_BOOKMARKS);
        assert_eq!(loaded.max_history, DEFAULT_MAX_HISTORY);
        assert_eq!(loaded.custom_theme, CustomTheme::default());
        assert!(loaded.addons.enabled);
        assert!(!loaded.addons.installed.is_empty());
        assert!(loaded.plugins.enabled);
        assert!(!loaded.plugins.installed.is_empty());
        assert!(!loaded.ai_browsing_enabled);
        assert_eq!(loaded.web_acceleration, WebAccelerationPolicy::Always);
        assert_eq!(loaded.media_playback, MediaPlaybackPolicy::Balanced);
        assert_eq!(loaded.startup_behavior, StartupBehavior::NewTabPage);
        assert_eq!(loaded.search_provider, SearchProvider::Google);
    }

    #[test]
    fn built_in_theme_ids_are_unique_and_resolvable() {
        let mut ids = std::collections::BTreeSet::new();
        for theme in BrowserTheme::ALL {
            assert!(ids.insert(theme.id()));
            assert_eq!(BrowserTheme::from_id(theme.id()), Some(theme));
            assert!(!theme.label().trim().is_empty());
            assert!(!theme.css_class().trim().is_empty());
        }
    }

    #[test]
    fn web_acceleration_policy_ids_are_unique_and_resolvable() {
        let mut ids = std::collections::BTreeSet::new();
        for policy in WebAccelerationPolicy::ALL {
            assert!(ids.insert(policy.id()));
            assert_eq!(WebAccelerationPolicy::from_id(policy.id()), Some(policy));
            assert!(!policy.label().trim().is_empty());
            assert!(!policy.description().trim().is_empty());
        }
    }

    #[test]
    fn profile_upgrades_legacy_on_demand_acceleration() {
        let mut profile = Profile {
            web_acceleration: WebAccelerationPolicy::OnDemand,
            ..Profile::default()
        };

        profile.enforce_limits();

        assert_eq!(profile.web_acceleration, WebAccelerationPolicy::Always);
    }

    #[test]
    fn media_playback_policy_ids_are_unique_and_resolvable() {
        let mut ids = std::collections::BTreeSet::new();
        for policy in MediaPlaybackPolicy::ALL {
            assert!(ids.insert(policy.id()));
            assert_eq!(MediaPlaybackPolicy::from_id(policy.id()), Some(policy));
            assert!(!policy.label().trim().is_empty());
            assert!(!policy.description().trim().is_empty());
        }
        assert!(!MediaPlaybackPolicy::Balanced.allows_capture());
        assert!(MediaPlaybackPolicy::Full.allows_capture());
        assert!(MediaPlaybackPolicy::Strict.requires_user_gesture());
    }

    #[test]
    fn startup_behavior_ids_are_unique_and_resolvable() {
        let mut ids = std::collections::BTreeSet::new();
        for behavior in StartupBehavior::ALL {
            assert!(ids.insert(behavior.id()));
            assert_eq!(StartupBehavior::from_id(behavior.id()), Some(behavior));
            assert!(!behavior.label().trim().is_empty());
        }
    }

    #[test]
    fn custom_theme_sanitizes_invalid_values() {
        let mut profile = Profile {
            custom_theme: CustomTheme {
                name: "  My Theme With A Very Long Name That Will Be Trimmed  ".to_string(),
                chrome: "bad".to_string(),
                page: "#010203".to_string(),
                surface: "112233".to_string(),
                text: "#ABCDEF".to_string(),
                muted: "missing".to_string(),
                accent: "#12xz99".to_string(),
                radius: CustomThemeRadius::Pill,
                density: CustomThemeDensity::Compact,
            },
            ..Profile::default()
        };

        profile.enforce_limits();

        assert_eq!(profile.custom_theme.name.len(), 32);
        assert_eq!(profile.custom_theme.chrome, CustomTheme::default().chrome);
        assert_eq!(profile.custom_theme.page, "#010203");
        assert_eq!(profile.custom_theme.surface, "#112233");
        assert_eq!(profile.custom_theme.text, "#abcdef");
        assert_eq!(profile.custom_theme.muted, CustomTheme::default().muted);
        assert_eq!(profile.custom_theme.accent, CustomTheme::default().accent);
        assert_eq!(profile.custom_theme.radius, CustomThemeRadius::Pill);
        assert_eq!(profile.custom_theme.density, CustomThemeDensity::Compact);
    }

    #[test]
    fn custom_theme_loads_when_new_fields_are_missing() {
        let value = serde_json::json!({
            "name": "Legacy Custom",
            "chrome": "#101010",
            "surface": "#202020",
            "text": "#f0f0f0",
            "accent": "#3366ff"
        });

        let theme: CustomTheme = serde_json::from_value(value).unwrap();

        assert_eq!(theme.page, CustomTheme::default().page);
        assert_eq!(theme.muted, CustomTheme::default().muted);
        assert_eq!(theme.radius, CustomThemeRadius::Rounded);
        assert_eq!(theme.density, CustomThemeDensity::Comfortable);
    }

    #[test]
    fn custom_theme_option_ids_are_unique_and_resolvable() {
        let mut radius_ids = std::collections::BTreeSet::new();
        for radius in CustomThemeRadius::ALL {
            assert!(radius_ids.insert(radius.id()));
            assert_eq!(CustomThemeRadius::from_id(radius.id()), Some(radius));
            assert!(!radius.label().is_empty());
        }

        let mut density_ids = std::collections::BTreeSet::new();
        for density in CustomThemeDensity::ALL {
            assert!(density_ids.insert(density.id()));
            assert_eq!(CustomThemeDensity::from_id(density.id()), Some(density));
            assert!(!density.label().is_empty());
        }
    }

    #[test]
    fn profile_enforces_history_and_bookmark_limits() {
        let mut profile = Profile {
            max_history: 2,
            max_bookmarks: 1,
            ..Profile::default()
        };

        for index in 0..4 {
            profile.record_visit(
                format!("Page {index}"),
                Url::parse(&format!("https://example.com/{index}")).unwrap(),
            );
        }
        assert_eq!(profile.history.len(), 2);
        assert_eq!(profile.history[0].title, "Page 2");

        assert!(profile.add_bookmark("One", Url::parse("https://one.example/").unwrap()));
        assert!(!profile.add_bookmark("Two", Url::parse("https://two.example/").unwrap()));
        assert_eq!(profile.bookmarks.len(), 1);

        for index in 0..(crate::privacy::DEFAULT_MAX_SITE_ALLOWLIST + 10) {
            profile
                .shields
                .site_allowlist
                .insert(format!("allow-{index}.example"));
        }
        for index in 0..(crate::privacy::DEFAULT_MAX_BLOCKED_HOSTS + 10) {
            profile
                .shields
                .blocked_hosts
                .insert(format!("blocked-{index}.example"));
        }
        profile.enforce_limits();
        assert_eq!(profile.max_history, 2);
        assert_eq!(profile.max_bookmarks, 1);
        assert_eq!(
            profile.shields.site_allowlist.len(),
            crate::privacy::DEFAULT_MAX_SITE_ALLOWLIST
        );
        assert_eq!(
            profile.shields.blocked_hosts.len(),
            crate::privacy::DEFAULT_MAX_BLOCKED_HOSTS
        );
    }

    #[test]
    fn profile_clamps_tampered_resource_maxima() {
        let mut profile = Profile {
            max_history: DEFAULT_MAX_HISTORY + 10_000,
            max_bookmarks: DEFAULT_MAX_BOOKMARKS + 10_000,
            ..Profile::default()
        };
        profile.enforce_limits();

        assert_eq!(profile.max_history, DEFAULT_MAX_HISTORY);
        assert_eq!(profile.max_bookmarks, DEFAULT_MAX_BOOKMARKS);

        profile.max_history = 0;
        profile.max_bookmarks = 0;
        profile.enforce_limits();

        assert_eq!(profile.max_history, 1);
        assert_eq!(profile.max_bookmarks, 1);
    }

    #[test]
    fn profile_enforces_safe_home_scheme() {
        let mut profile = Profile {
            home: Url::parse("file:///etc/passwd").unwrap(),
            ..Profile::default()
        };

        profile.enforce_limits();

        assert_eq!(profile.home, default_home_url());
    }
}
