/// Storage simulation — localStorage, sessionStorage, document.cookie.
///
use std::collections::HashMap;
use std::sync::{Arc, Mutex};

/// Simulated Web Storage (localStorage/sessionStorage).
#[derive(Debug, Clone, Default)]
pub struct WebStorage {
    data: Arc<Mutex<HashMap<String, String>>>,
    max_items: usize,
}

impl WebStorage {
    pub fn new(max_items: usize) -> Self {
        Self {
            data: Arc::new(Mutex::new(HashMap::new())),
            max_items,
        }
    }

    pub fn get_item(&self, key: &str) -> Option<String> {
        self.data.lock().ok()?.get(key).cloned()
    }

    pub fn set_item(&self, key: &str, value: &str) -> Result<(), String> {
        let mut data = self.data.lock().map_err(|e| e.to_string())?;
        if data.len() >= self.max_items && !data.contains_key(key) {
            return Err("QuotaExceededError".into());
        }
        data.insert(key.to_string(), value.to_string());
        Ok(())
    }

    pub fn remove_item(&self, key: &str) {
        if let Ok(mut data) = self.data.lock() {
            data.remove(key);
        }
    }

    pub fn clear(&self) {
        if let Ok(mut data) = self.data.lock() {
            data.clear();
        }
    }

    pub fn length(&self) -> usize {
        self.data.lock().map(|d| d.len()).unwrap_or(0)
    }

    pub fn key(&self, index: usize) -> Option<String> {
        self.data.lock().ok()?.keys().nth(index).cloned()
    }
}

/// Simulated cookie jar.
#[derive(Debug, Clone)]
pub struct CookieJar {
    cookies: Arc<Mutex<HashMap<String, CookieEntry>>>,
}

#[derive(Debug, Clone)]
struct CookieEntry {
    value: String,
    domain: String,
    path: String,
    secure: bool,
    http_only: bool,
    same_site: String,
}

impl Default for CookieJar {
    fn default() -> Self {
        Self::new()
    }
}

impl CookieJar {
    pub fn new() -> Self {
        Self {
            cookies: Arc::new(Mutex::new(HashMap::new())),
        }
    }

    /// Pre-populate with cookies (for testing extension cookie access).
    pub fn add(&self, name: &str, value: &str, domain: &str) {
        if let Ok(mut jar) = self.cookies.lock() {
            jar.insert(
                name.to_string(),
                CookieEntry {
                    value: value.to_string(),
                    domain: domain.to_string(),
                    path: "/".into(),
                    secure: false,
                    http_only: false,
                    same_site: "Lax".into(),
                },
            );
        }
    }

    /// Parse and set a cookie from a `document.cookie = "..."` string.
    pub fn set_from_string(&self, cookie_str: &str) {
        let parts: Vec<&str> = cookie_str.split(';').map(|s| s.trim()).collect();
        let Some(name_value) = parts.first() else {
            return;
        };
        let (name, value) = name_value.split_once('=').unwrap_or((name_value, ""));

        let mut entry = CookieEntry {
            value: value.to_string(),
            domain: String::new(),
            path: "/".into(),
            secure: false,
            http_only: false,
            same_site: "Lax".into(),
        };

        for part in &parts[1..] {
            let lower = part.to_ascii_lowercase();
            if let Some(domain) = lower.strip_prefix("domain=") {
                entry.domain = domain.to_string();
            } else if let Some(path) = lower.strip_prefix("path=") {
                entry.path = path.to_string();
            } else if lower == "secure" {
                entry.secure = true;
            } else if lower == "httponly" {
                entry.http_only = true;
            } else if lower.starts_with("samesite=") {
                entry.same_site = part[9..].to_string();
            }
        }

        if let Ok(mut jar) = self.cookies.lock() {
            jar.insert(name.trim().to_string(), entry);
        }
    }

    /// Get the `document.cookie` string (non-HttpOnly cookies).
    pub fn to_cookie_string(&self) -> String {
        let jar = match self.cookies.lock() {
            Ok(j) => j,
            Err(_) => return String::new(),
        };
        jar.iter()
            .filter(|(_, e)| !e.http_only)
            .map(|(name, entry)| format!("{}={}", name, entry.value))
            .collect::<Vec<_>>()
            .join("; ")
    }

    /// Get all cookies as JSON (for chrome.cookies API).
    pub fn to_json(&self) -> String {
        let jar = match self.cookies.lock() {
            Ok(j) => j,
            Err(_) => return "[]".into(),
        };
        let entries: Vec<serde_json::Value> = jar
            .iter()
            .map(|(name, entry)| {
                serde_json::json!({
                    "name": name,
                    "value": entry.value,
                    "domain": entry.domain,
                    "path": entry.path,
                    "secure": entry.secure,
                    "httpOnly": entry.http_only,
                    "sameSite": entry.same_site,
                })
            })
            .collect();
        serde_json::to_string(&entries).unwrap_or_else(|_| "[]".into())
    }
}

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

    #[test]
    fn local_storage_crud() {
        let storage = WebStorage::new(100);
        storage.set_item("key", "value").unwrap();
        assert_eq!(storage.get_item("key"), Some("value".into()));
        assert_eq!(storage.length(), 1);
        storage.remove_item("key");
        assert_eq!(storage.length(), 0);
    }

    #[test]
    fn cookie_set_and_read() {
        let jar = CookieJar::new();
        jar.set_from_string("session=abc123; Path=/; Secure");
        let cookies = jar.to_cookie_string();
        assert!(cookies.contains("session=abc123"));
    }

    #[test]
    fn http_only_cookies_hidden_from_document() {
        let jar = CookieJar::new();
        jar.set_from_string("visible=yes");
        jar.set_from_string("secret=no; HttpOnly");
        let cookies = jar.to_cookie_string();
        assert!(cookies.contains("visible=yes"));
        assert!(!cookies.contains("secret=no"));
    }
}
