mvsep-gui 1.2.1

MVSEP GUI - Music separation desktop application
use rusqlite::{params, Connection};
use serde::{Deserialize, Serialize};
use std::path::Path;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WebConfigEntry {
    pub key: String,
    pub value: String,
    pub updated_at: String,
}

pub struct WebDatabase {
    conn: Connection,
}

impl WebDatabase {
    pub fn new(path: &str) -> anyhow::Result<Self> {
        let conn = Connection::open(path)?;
        let db = Self { conn };
        db.init()?;
        Ok(db)
    }

    fn init(&self) -> anyhow::Result<()> {
        self.conn.execute_batch(
            "
            CREATE TABLE IF NOT EXISTS web_config (
                key TEXT PRIMARY KEY NOT NULL,
                value TEXT NOT NULL DEFAULT '',
                updated_at TEXT DEFAULT (datetime('now'))
            );

            CREATE INDEX IF NOT EXISTS idx_web_config_key ON web_config(key);
            ",
        )?;
        Ok(())
    }

    pub fn set_string(&self, key: &str, value: &str) -> anyhow::Result<()> {
        self.conn.execute(
            "INSERT OR REPLACE INTO web_config (key, value, updated_at) VALUES (?1, ?2, datetime('now'))",
            params![key, value],
        )?;
        Ok(())
    }

    pub fn get_string(&self, key: &str) -> anyhow::Result<Option<String>> {
        let mut stmt = self.conn.prepare("SELECT value FROM web_config WHERE key = ?1")?;
        let mut rows = stmt.query_map(params![key], |row| row.get(0))?;
        match rows.next() {
            Some(result) => Ok(Some(result?)),
            None => Ok(None),
        }
    }

    pub fn delete(&self, key: &str) -> anyhow::Result<bool> {
        let rows_affected = self.conn.execute("DELETE FROM web_config WHERE key = ?1", params![key])?;
        Ok(rows_affected > 0)
    }

    pub fn exists(&self, key: &str) -> anyhow::Result<bool> {
        let mut stmt = self.conn.prepare("SELECT COUNT(*) FROM web_config WHERE key = ?1")?;
        let count: i64 = stmt.query_row(params![key], |row| row.get(0))?;
        Ok(count > 0)
    }

    pub fn get_all(&self) -> anyhow::Result<Vec<WebConfigEntry>> {
        let mut stmt = self.conn.prepare("SELECT key, value, updated_at FROM web_config ORDER BY key")?;
        let rows = stmt.query_map([], |row| {
            Ok(WebConfigEntry {
                key: row.get(0)?,
                value: row.get(1)?,
                updated_at: row.get(2)?,
            })
        })?;
        rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.into())
    }

    pub fn clear_all(&self) -> anyhow::Result<u64> {
        let affected = self.conn.execute("DELETE FROM web_config", [])?;
        Ok(affected as u64)
    }
}

pub fn open_web_database(db_path: &Path) -> anyhow::Result<WebDatabase> {
    if let Some(parent) = db_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    WebDatabase::new(&db_path.to_string_lossy())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use std::sync::atomic::{AtomicUsize, Ordering};

    static TEST_COUNTER: AtomicUsize = AtomicUsize::new(0);

    fn create_temp_db_path() -> String {
        let temp_dir = std::env::temp_dir();
        let counter = TEST_COUNTER.fetch_add(1, Ordering::Relaxed);
        let unique_name = format!("mvsep_web_db_test_{}_{}.db", std::process::id(), counter);
        let path = temp_dir.join(unique_name);
        if path.exists() {
            fs::remove_file(&path).unwrap();
        }
        path.to_string_lossy().to_string()
    }

    #[test]
    fn test_set_and_get_string() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        db.set_string("locale", "zh-CN").unwrap();
        let result = db.get_string("locale").unwrap();
        assert_eq!(result, Some("zh-CN".to_string()));
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_get_nonexistent_key() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        let result = db.get_string("nonexistent").unwrap();
        assert_eq!(result, None);
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_update_existing_key() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        db.set_string("theme", "dark").unwrap();
        db.set_string("theme", "fresh-cyan").unwrap();
        let result = db.get_string("theme").unwrap();
        assert_eq!(result, Some("fresh-cyan".to_string()));
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_delete_key() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        db.set_string("test_key", "test_value").unwrap();
        assert!(db.exists("test_key").unwrap());
        
        let deleted = db.delete("test_key").unwrap();
        assert!(deleted);
        assert!(!db.exists("test_key").unwrap());
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_get_all_entries() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        db.set_string("key1", "value1").unwrap();
        db.set_string("key2", "value2").unwrap();
        
        let entries = db.get_all().unwrap();
        assert_eq!(entries.len(), 2);
        
        let keys: Vec<String> = entries.into_iter().map(|e| e.key).collect();
        assert!(keys.contains(&"key1".to_string()));
        assert!(keys.contains(&"key2".to_string()));
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_clear_all() {
        let db_path = create_temp_db_path();
        let db = WebDatabase::new(&db_path).unwrap();
        
        db.set_string("key1", "value1").unwrap();
        db.set_string("key2", "value2").unwrap();
        
        let cleared = db.clear_all().unwrap();
        assert_eq!(cleared, 2);
        
        let entries = db.get_all().unwrap();
        assert_eq!(entries.len(), 0);
        
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_persistence_across_restarts() {
        let db_path = create_temp_db_path();
        
        {
            let db = WebDatabase::new(&db_path).unwrap();
            db.set_string("theme", "dark").unwrap();
            db.set_string("locale", "en-US").unwrap();
        }
        
        {
            let db = WebDatabase::new(&db_path).unwrap();
            assert_eq!(db.get_string("theme").unwrap(), Some("dark".to_string()));
            assert_eq!(db.get_string("locale").unwrap(), Some("en-US".to_string()));
        }
        
        fs::remove_file(&db_path).unwrap();
    }
}