pub mod models;
pub mod schema;
use models::{CmdType, ExtDataField, ExtDataSearchQuery, SearchMode};
use rusqlite::{params, params_from_iter, Connection, Result, ToSql};
use serde_json::{json, Value};
use std::path::Path;

pub const DB_VERSION: u32 = 1;

pub fn get_connection<P: AsRef<Path>>(
    file_path: P,
    encryption_key: Option<String>,
) -> Result<Connection> {
    let conn = Connection::open(file_path)?;
    if let Some(encryption_key) = encryption_key {
        conn.pragma_update(None, "key", &encryption_key)?;
        conn.pragma_update(None, "foreign_keys", "ON")?;
    }
    Ok(conn)
}

#[derive(Debug)]
pub struct JarvisDB {
    pub conn: Connection,
}

impl JarvisDB {
    pub fn new<P: AsRef<Path>>(file_path: P, encryption_key: Option<String>) -> Result<Self> {
        let conn = get_connection(file_path, encryption_key)?;
        Ok(Self { conn })
    }

    /**
     * Run this when app starts everytime, to ensure the db is up to date.
     */
    pub fn init(&self) -> Result<()> {
        let schema_version_exists = self.schema_version_exists()?;
        if !schema_version_exists {
            // this means the db is not initialized at all, so we need to migrate from version 0, i.e. run all migration scripts
            self.migrate_after_version(0)?;
        } else {
            let current_version = self.get_schema_version()?;
            self.migrate_after_version(current_version.unwrap())?;
        }
        Ok(())
    }

    pub fn migrate_after_version(&self, version: u16) -> Result<()> {
        for migration in schema::MIGRATIONS.iter() {
            if migration.version > version {
                println!(
                    "Migrating from version {} to {}",
                    version, migration.version
                );
                // self.conn.execute(&migration.schema, params![])?;
                match self
                    .conn
                    .execute_batch(&format!("BEGIN; {} COMMIT;", migration.script))
                {
                    Ok(_) => {
                        self.upsert_schema_version(migration.version)?;
                    }
                    Err(e) => {
                        eprintln!("Failed to execute migration script: {}", e);
                        return Err(e);
                    }
                }
            }
        }
        Ok(())
    }

    /**
     * Insert the schema version into the schema_version table if it doesn't exist yet
     */
    pub fn upsert_schema_version(&self, version: u16) -> Result<()> {
        self.conn.execute(
            "INSERT OR IGNORE INTO schema_version (version) VALUES (?1)",
            params![version],
        )?;
        Ok(())
    }

    pub fn schema_version_exists(&self) -> Result<bool> {
        match self.get_schema_version() {
            Ok(Some(_)) => Ok(true),
            _ => Ok(false),
        }
    }

    pub fn get_schema_version(&self) -> Result<Option<u16>> {
        let mut stmt = self
            .conn
            .prepare("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1;")?;
        let version_iter = stmt.query_map(params![], |row| {
            let version: u16 = row.get(0)?;
            Ok(version)
        })?;
        let mut versions: Vec<u16> = Vec::new();
        for version in version_iter {
            versions.push(version?);
        }
        Ok(versions.first().copied())
    }

    /* -------------------------------------------------------------------------- */
    /*                               Extensions CRUD                              */
    /* -------------------------------------------------------------------------- */

    pub fn create_extension(
        &self,
        identifier: &str,
        version: &str,
        enabled: bool,
        path: Option<&str>,
        data: Option<&str>,
    ) -> Result<()> {
        self.conn.execute(
            "INSERT INTO extensions (identifier, version, enabled, path, data) VALUES (?1, ?2, ?3, ?4, ?5)",
            params![identifier, version, enabled, path, data],
        )?;
        Ok(())
    }

    pub fn get_all_extensions(&self) -> Result<Vec<models::Ext>> {
        let mut stmt = self.conn.prepare(
            "SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions",
        )?;
        let ext_iter = stmt.query_map(params![], |row| {
            Ok(models::Ext {
                ext_id: row.get(0)?,
                identifier: row.get(1)?,
                path: row.get(2)?,
                data: row.get(3)?,
                version: row.get(4)?,
                enabled: row.get(5)?,
                installed_at: row.get(6)?,
            })
        })?;
        let mut exts = Vec::new();
        for ext in ext_iter {
            exts.push(ext?);
        }
        Ok(exts)
    }

    /**
     * Get the first extension by identifier, if there are multiple, it will return an error
     */
    pub fn get_unique_extension_by_identifier(
        &self,
        identifier: &str,
    ) -> anyhow::Result<Option<models::Ext>> {
        let exts = self.get_all_extensions_by_identifier(identifier)?;
        if exts.len() > 1 {
            return Err(anyhow::anyhow!(
                "Multiple extensions with the same identifier: {}",
                identifier
            ));
        }
        Ok(exts.first().cloned())
    }

    pub fn get_all_extensions_by_identifier(&self, identifier: &str) -> Result<Vec<models::Ext>> {
        let mut stmt = self.conn.prepare(
            "SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions WHERE identifier = ?1",
        )?;
        let ext_iter = stmt.query_map(params![identifier], |row| {
            Ok(models::Ext {
                ext_id: row.get(0)?,
                identifier: row.get(1)?,
                path: row.get(2)?,
                data: row.get(3)?,
                version: row.get(4)?,
                enabled: row.get(5)?,
                installed_at: row.get(6)?,
            })
        })?;
        let mut exts = Vec::new();
        for ext in ext_iter {
            exts.push(ext?);
        }
        Ok(exts)
    }

    pub fn get_unique_extension_by_path(&self, path: &str) -> Result<Option<models::Ext>> {
        let mut stmt = self.conn.prepare(
            "SELECT ext_id, identifier, path, data, version, enabled, installed_at FROM extensions WHERE path = ?1",
        )?;
        let ext_iter = stmt.query_map(params![path], |row| {
            Ok(models::Ext {
                ext_id: row.get(0)?,
                identifier: row.get(1)?,
                path: row.get(2)?,
                data: row.get(3)?,
                version: row.get(4)?,
                enabled: row.get(5)?,
                installed_at: row.get(6)?,
            })
        })?;
        let mut exts = Vec::new();
        for ext in ext_iter {
            exts.push(ext?);
        }
        Ok(exts.first().cloned())
    }

    // TODO: clean this up
    // pub fn delete_extension_by_identifier(&self, identifier: &str) -> Result<()> {
    //     self.conn.execute(
    //         "DELETE FROM extensions WHERE identifier = ?1",
    //         params![identifier],
    //     )?;
    //     Ok(())
    // }

    pub fn delete_extension_by_path(&self, path: &str) -> Result<()> {
        println!("DB deleting extension by path: {}", path);
        self.conn
            .execute("DELETE FROM extensions WHERE path = ?1", params![path])?;
        Ok(())
    }

    pub fn delete_extension_by_ext_id(&self, ext_id: i32) -> Result<()> {
        self.conn
            .execute("DELETE FROM extensions WHERE ext_id = ?1", params![ext_id])?;
        Ok(())
    }

    /* -------------------------------------------------------------------------- */
    /*                                Command CRUD                                */
    /* -------------------------------------------------------------------------- */
    pub fn create_command(
        &self,
        ext_id: i32,
        name: &str,
        cmd_type: CmdType,
        data: &str,
        enabled: bool,
        alias: Option<&str>,
        hotkey: Option<&str>,
    ) -> Result<()> {
        self.conn.execute(
            "INSERT INTO commands (ext_id, name, type, data, alias, hotkey, enabled) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
            params![ext_id, name, cmd_type.to_string(), data, alias, hotkey, enabled],
        )?;
        Ok(())
    }

    pub fn get_command_by_id(&self, cmd_id: i32) -> Result<Option<models::Cmd>> {
        let mut stmt = self
            .conn
            .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled FROM commands WHERE cmd_id = ?1")?;
        let cmd_iter = stmt.query_map(params![cmd_id], |row| {
            Ok(models::Cmd {
                cmd_id: row.get(0)?,
                ext_id: row.get(1)?,
                name: row.get(2)?,
                type_: row.get(3)?,
                data: row.get(4)?,
                alias: row.get(5)?,
                hotkey: row.get(6)?,
                enabled: row.get(7)?,
            })
        })?;
        let mut cmds = Vec::new();
        for cmd in cmd_iter {
            cmds.push(cmd?);
        }
        Ok(cmds.first().cloned())
    }

    pub fn get_commands_by_ext_id(&self, ext_id: i32) -> Result<Vec<models::Cmd>> {
        let mut stmt = self
            .conn
            .prepare("SELECT cmd_id, ext_id, name, type, data, alias, hotkey, enabled FROM commands WHERE ext_id = ?1")?;
        let cmd_iter = stmt.query_map(params![ext_id], |row| {
            Ok(models::Cmd {
                cmd_id: row.get(0)?,
                ext_id: row.get(1)?,
                name: row.get(2)?,
                type_: row.get(3)?,
                data: row.get(4)?,
                alias: row.get(5)?,
                hotkey: row.get(6)?,
                enabled: row.get(7)?,
            })
        })?;
        let mut cmds = Vec::new();
        for cmd in cmd_iter {
            cmds.push(cmd?);
        }
        Ok(cmds)
    }

    pub fn delete_command_by_id(&self, cmd_id: i32) -> Result<()> {
        self.conn
            .execute("DELETE FROM commands WHERE cmd_id = ?1", params![cmd_id])?;
        Ok(())
    }

    pub fn update_command_by_id(
        &self,
        cmd_id: i32,
        name: &str,
        cmd_type: CmdType,
        data: &str,
        enabled: bool,
        alias: Option<&str>,
        hotkey: Option<&str>,
    ) -> Result<()> {
        self.conn.execute(
            "UPDATE commands SET name = ?1, type = ?2, data = ?3, alias = ?4, hotkey = ?5, enabled = ?6 WHERE cmd_id = ?7",
            params![name, cmd_type.to_string(), data, alias, hotkey, enabled, cmd_id],
        )?;
        Ok(())
    }

    /* -------------------------------------------------------------------------- */
    /*                            Extension Data CRUD                            */
    /* -------------------------------------------------------------------------- */
    pub fn create_extension_data(
        &self,
        ext_id: i32,
        data_type: &str,
        data: &str,
        search_text: Option<&str>,
        metadata: Option<&str>,
    ) -> Result<()> {
        self.conn.execute(
            "INSERT INTO extension_data (ext_id, data_type, data, search_text, metadata) VALUES (?1, ?2, ?3, ?4, ?5)",
            params![ext_id, data_type, data, search_text, metadata],
        )?;
        Ok(())
    }

    pub fn get_extension_data_by_id(
        &self,
        data_id: i32,
        mut fields: Option<Vec<ExtDataField>>,
    ) -> Result<Option<models::ExtData>> {
        if fields.is_none() {
            fields = Some(vec![ExtDataField::Data, ExtDataField::SearchText]);
        }
        let contains_data_field = fields.as_ref().map_or(false, |fields| {
            fields.iter().any(|f| f == &ExtDataField::Data)
        });
        let contains_search_text_field = fields.as_ref().map_or(false, |fields| {
            fields.iter().any(|f| f == &ExtDataField::SearchText)
        });
        let mut query = String::from("SELECT data_id, ext_id, data_type, created_at, updated_at");
        if contains_data_field {
            query.push_str(", data");
        }
        if contains_search_text_field {
            query.push_str(", search_text");
        }
        query.push_str(
            " FROM extension_data
             WHERE data_id = ?1",
        );
        let mut stmt = self.conn.prepare(&query)?;
        let ext_data_iter = stmt.query_map(params![data_id], |row| {
            Ok(models::ExtData {
                data_id: row.get(0)?,
                ext_id: row.get(1)?,
                data_type: row.get(2)?,
                created_at: row.get(3)?,
                updated_at: row.get(4)?,
                data: match contains_data_field {
                    true => Some(row.get(5)?),
                    false => None,
                },
                search_text: match contains_search_text_field {
                    true => row.get(5 + contains_data_field as usize)?, // if contains_data_field is true, search_text is at index 6, otherwise 5
                    false => None,
                },
            })
        })?;
        let mut ext_data = Vec::new();
        for data in ext_data_iter {
            ext_data.push(data?);
        }
        Ok(ext_data.first().cloned())
    }

    /// Sample Queries With Different Search Modes
    /// ## Full Text Search (FTS) Mode:
    /// ```sql
    /// SELECT extension_data.*
    /// FROM extension_data
    /// JOIN extension_data_fts ON extension_data.data_id = extension_data_fts.data_id
    /// WHERE extension_data_fts.search_text MATCH 'extension_data';
    /// ```
    /// ## Exact Match Mode:
    /// ```sql
    /// SELECT extension_data.*
    /// FROM extension_data
    /// WHERE extension_data.search_text = 'extension_data';
    /// ```
    /// ## Like Mode:
    /// ```sql
    /// SELECT extension_data.*
    /// FROM extension_data
    /// WHERE extension_data.search_text LIKE '%extension_data%';
    /// ```
    pub fn search_extension_data(
        &self,
        search_query: ExtDataSearchQuery,
    ) -> Result<Vec<models::ExtData>> {
        let mut fields = search_query.fields;
        if fields.is_none() {
            fields = Some(vec![ExtDataField::Data, ExtDataField::SearchText]);
        }
        let contains_data_field = fields.as_ref().map_or(false, |fields| {
            fields.iter().any(|f| f == &ExtDataField::Data)
        });
        let contains_search_text_field = fields.as_ref().map_or(false, |fields| {
            fields.iter().any(|f| f == &ExtDataField::SearchText)
        });
        let mut query = String::from("SELECT ed.data_id as data_id, ed.ext_id as ext_id, ed.data_type as data_type, ed.created_at as created_at, ed.updated_at as updated_at");
        if contains_data_field {
            query.push_str(", ed.data as data");
        }
        if contains_search_text_field {
            query.push_str(", ed.search_text as search_text");
        }
        query.push_str(" FROM extension_data ed");
        if search_query.search_mode == SearchMode::FTS {
            query.push_str(" JOIN extension_data_fts edf ON ed.data_id = edf.data_id");
        }
        query.push_str(" WHERE ed.ext_id = ?1");
        let mut params: Vec<Box<dyn ToSql>> = vec![Box::new(search_query.ext_id)];
        let mut param_index = 2;

        if let Some(di) = search_query.data_id {
            query.push_str(&format!(" AND data_id = ?{}", param_index));
            params.push(Box::new(di));
            param_index += 1;
        }

        if let Some(dt) = search_query.data_type {
            query.push_str(&format!(" AND data_type = ?{}", param_index));
            params.push(Box::new(dt));
            param_index += 1;
        }

        if let Some(st) = search_query.search_text {
            match search_query.search_mode {
                SearchMode::ExactMatch => {
                    query.push_str(&format!(" AND ed.search_text = ?{}", param_index));
                    params.push(Box::new(st));
                }
                SearchMode::Like => {
                    query.push_str(&format!(" AND ed.search_text LIKE ?{}", param_index));
                    params.push(Box::new(format!("{}", st)));
                }
                SearchMode::FTS => {
                    // Join with FTS table and use MATCH operator
                    query.push_str(&format!(" AND edf.search_text MATCH ?{}", param_index));
                    params.push(Box::new(st));
                }
            }
            param_index += 1;
        }

        if let Some(after) = search_query.after_created_at {
            query.push_str(&format!(" AND created_at > ?{}", param_index));
            params.push(Box::new(after));
            param_index += 1;
        }

        if let Some(before) = search_query.before_created_at {
            query.push_str(&format!(" AND created_at < ?{}", param_index));
            params.push(Box::new(before));
            param_index += 1;
        }

        if let Some(order_by_created_at) = search_query.order_by_created_at {
            query.push_str(&format!(
                " ORDER BY created_at {}",
                order_by_created_at.to_string()
            ));
        }

        if let Some(order_by_updated_at) = search_query.order_by_updated_at {
            query.push_str(&format!(
                " ORDER BY updated_at {}",
                order_by_updated_at.to_string()
            ));
        }

        if let Some(limit) = search_query.limit {
            query.push_str(&format!(" LIMIT ?{}", param_index));
            params.push(Box::new(limit));
            param_index += 1;
        }
        if let Some(offset) = search_query.offset {
            query.push_str(&format!(" OFFSET ?{}", param_index));
            params.push(Box::new(offset));
            param_index += 1;
        }
        let mut stmt = self.conn.prepare(&query)?;
        let ext_data_iter =
            stmt.query_map(params_from_iter(params.iter().map(|p| p.as_ref())), |row| {
                Ok(models::ExtData {
                    data_id: row.get(0)?,
                    ext_id: row.get(1)?,
                    data_type: row.get(2)?,
                    created_at: row.get(3)?,
                    updated_at: row.get(4)?,
                    data: match contains_data_field {
                        true => Some(row.get(5)?),
                        false => None,
                    },
                    search_text: match contains_search_text_field {
                        true => row.get(5 + contains_data_field as usize)?, // if contains_data_field is true, search_text is at index 6, otherwise 5
                        false => None,
                    },
                })
            })?;

        let mut ext_data = Vec::new();
        for data in ext_data_iter {
            ext_data.push(data?);
        }
        Ok(ext_data)
    }

    pub fn delete_extension_data_by_id(&self, data_id: i32) -> Result<()> {
        self.conn.execute(
            "DELETE FROM extension_data WHERE data_id = ?1",
            params![data_id],
        )?;
        Ok(())
    }

    pub fn update_extension_data_by_id(
        &self,
        data_id: i32,
        data: &str,
        search_text: Option<&str>,
    ) -> Result<()> {
        self.conn.execute(
            "UPDATE extension_data SET data = ?1, search_text = ?2 WHERE data_id = ?3",
            params![data, search_text, data_id],
        )?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::{fs, path::PathBuf};
    use tempfile::tempdir;

    #[test]
    fn test_get_connection() {
        let dir = tempdir().unwrap();
        let db_path = dir.path().join("test.db");
        let _conn = get_connection(&db_path, None).unwrap();
        assert!(fs::metadata(&db_path).is_ok());
        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_extension_crud() {
        let dir = tempdir().unwrap();
        let db_path = dir.path().join("test.db");
        // create database and initialize
        let db = JarvisDB::new(&db_path, None).unwrap();
        assert!(fs::metadata(&db_path).is_ok());
        db.init().unwrap();
        db.create_extension("test", "0.1.0", true, Some("/abc/def"), None)
            .unwrap();
        let exts = db.get_all_extensions().unwrap();
        assert_eq!(exts.len(), 1);

        // expect error due to unique identifier constraint
        assert!(db
            .create_extension("test", "0.1.0", true, Some("/abc/def"), None)
            .is_err());

        // get ext by identifier
        let ext = db.get_unique_extension_by_identifier("test").unwrap();
        assert!(ext.is_some());
        let ext = ext.unwrap();
        assert_eq!(ext.identifier, "test");
        assert_eq!(ext.version, "0.1.0");
        assert_eq!(ext.enabled, true);
        assert_eq!(ext.installed_at.len(), 19);

        // get ext by identifier that does not exist
        let ext = db.get_unique_extension_by_identifier("test2").unwrap();
        assert!(ext.is_none());

        /* ----------------------- Delete ext by identifier ---------------------- */
        // db.delete_extension_by_identifier("test").unwrap();
        db.delete_extension_by_path("/abc/def").unwrap();
        let exts = db.get_all_extensions().unwrap();
        assert_eq!(exts.len(), 0);

        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_ext_data_crud() {
        // let dir = tempdir().unwrap();
        // let db_path = dir.path().join("test.db");
        let db_path = PathBuf::from("./test.db");
        if db_path.exists() {
            fs::remove_file(&db_path).unwrap();
        }

        // create database and initialize
        let db = JarvisDB::new(&db_path, None).unwrap();
        assert!(fs::metadata(&db_path).is_ok());
        db.init().unwrap();
        db.create_extension("test", "0.1.0", true, Some("/abc/def"), None)
            .unwrap();
        let ext = db
            .get_unique_extension_by_identifier("test")
            .unwrap()
            .unwrap();

        db.create_extension_data(ext.ext_id, "test", "{}", None, None)
            .unwrap();
        db.create_extension_data(ext.ext_id, "setting", "{}", None, None)
            .unwrap();
        /* ---------------------- Search with data_type == test --------------------- */
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("test".to_string()),
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();

        assert_eq!(ext_data.len(), 1); // there is only one record with data_type == test

        /* ------------------------ Search without any filter ----------------------- */
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: None,
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 2); // one test, one setting

        // /* -------------------------- Test Full Text Search ------------------------- */
        let json_data = json!({
            "name": "John Doe",
            "age": 43,
            "phones": [
                "+44 1234567",
                "+44 2345678"
            ]
        });
        db.create_extension_data(
            ext.ext_id,
            "data",
            "{}",
            Some("hello world from rust"),
            Some(json_data.to_string().as_str()),
        )
        .unwrap();
        db.create_extension_data(
            ext.ext_id,
            "data",
            "{}",
            Some("world is a mess"),
            Some(json_data.to_string().as_str()),
        )
        .unwrap();
        // Search Mode: Like
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::Like,
                data_type: Some("data".to_string()),
                search_text: Some("worl%".to_string()),
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 1);
        /* ----------------------- both record contains world ----------------------- */
        // Search Mode: FTS
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("data".to_string()),
                search_text: Some("wOrLd".to_string()), // FTS is case insensitive
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 2);
        /* ------------------------ search for rust with FTS ------------------------ */
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("data".to_string()),
                search_text: Some("rust".to_string()),
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 1);

        // get ext data with search text that does not exist
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("test".to_string()),
                search_text: Some("test".to_string()),
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 0);

        /* ---------------- All 4 test records are created after 2021 --------------- */
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: None,
                search_text: None,
                after_created_at: Some("2021-01-01".to_string()),
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 4);

        // no filter, get all records
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: None,
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 4);

        /* --------------- All 4 test records are created before 2030 --------------- */
        // if this code still runs in 2030, I will be very happy to fix this test
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: None,
                search_text: None,
                after_created_at: None,
                before_created_at: Some("2030-01-01".to_string()),
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 4);

        // get ext data with created_at filter that does not exist
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::ExactMatch,
                data_type: None,
                search_text: None,
                after_created_at: Some("2021-01-01".to_string()),
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 4);

        /* ---------------------- Delete ext data by data_id ---------------------- */
        // there is only one record with data_type == setting
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("setting".to_string()),
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        let data_id = ext_data.first().unwrap().data_id;
        db.delete_extension_data_by_id(data_id).unwrap();
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("setting".to_string()),
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        assert_eq!(ext_data.len(), 0);

        /* ---------------------- Update ext data by data_id ---------------------- */
        let ext_data = db
            .search_extension_data(ExtDataSearchQuery {
                ext_id: ext.ext_id,
                fields: None,
                data_id: None,
                search_mode: SearchMode::FTS,
                data_type: Some("data".to_string()),
                search_text: None,
                after_created_at: None,
                before_created_at: None,
                order_by_created_at: None,
                order_by_updated_at: None,
                limit: None,
                offset: None,
            })
            .unwrap();
        let data_id = ext_data.first().unwrap().data_id;
        db.update_extension_data_by_id(data_id, "{\"name\": \"huakun\"}", Some("updated"))
            .unwrap();
        let ext_data = db.get_extension_data_by_id(data_id, None).unwrap();
        assert!(ext_data.is_some());
        let ext_data = ext_data.unwrap();
        assert_eq!(ext_data.data.unwrap(), "{\"name\": \"huakun\"}");

        /* ----------------------------- Optional Fields ---------------------------- */
        // let ext_data = db
        //     .search_extension_data(ext.ext_id, false, None, Some("data"), None, None, None, None, None, None, vec![])
        //     .unwrap();

        fs::remove_file(&db_path).unwrap();
    }

    #[test]
    fn test_command_crud() {
        let dir = tempdir().unwrap();
        let db_path = dir.path().join("test.db");
        // create database and initialize
        let db = JarvisDB::new(&db_path, None).unwrap();
        assert!(fs::metadata(&db_path).is_ok());
        db.init().unwrap();
        db.create_extension("test", "0.1.0", true, None, None)
            .unwrap();
        let ext = db
            .get_unique_extension_by_identifier("test")
            .unwrap()
            .unwrap();

        db.create_command(ext.ext_id, "test", CmdType::Iframe, "{}", true, None, None)
            .unwrap();
        db.create_command(
            ext.ext_id,
            "test2",
            CmdType::UiWorker,
            "{}",
            true,
            Some("t2"),
            Some("t2"),
        )
        .unwrap();
        /* ---------------------- Get command by id ---------------------- */
        let cmd = db.get_command_by_id(1).unwrap().unwrap();
        assert_eq!(cmd.name, "test");
        assert_eq!(cmd.type_, models::CmdType::Iframe);
        assert_eq!(cmd.data, "{}");

        /* ---------------------- Get commands by ext_id ---------------------- */
        let cmds = db.get_commands_by_ext_id(ext.ext_id).unwrap();
        assert_eq!(cmds.len(), 2);

        // test command test2's alias and hotkey
        let cmd = db.get_command_by_id(cmds[1].cmd_id).unwrap().unwrap();
        assert_eq!(cmd.alias.unwrap(), "t2");
        assert_eq!(cmd.hotkey.unwrap(), "t2");

        /* ---------------------- Delete command by id ---------------------- */
        db.delete_command_by_id(1).unwrap();
        let cmds = db.get_commands_by_ext_id(ext.ext_id).unwrap();
        assert_eq!(cmds.len(), 1);

        /* ---------------------- Update command by id ---------------------- */
        db.update_command_by_id(
            cmds[0].cmd_id,
            "test3",
            CmdType::UiWorker,
            "{}",
            false,
            Some("alias"),
            Some("Command+U"),
        )
        .unwrap();
        let cmd = db.get_command_by_id(cmds[0].cmd_id).unwrap().unwrap();
        assert_eq!(cmd.name, "test3");
        assert_eq!(cmd.type_, models::CmdType::UiWorker);
        assert_eq!(cmd.data, "{}");
        assert_eq!(cmd.enabled, false);
        assert_eq!(cmd.alias.unwrap(), "alias");
        assert_eq!(cmd.hotkey.unwrap(), "Command+U");

        fs::remove_file(&db_path).unwrap();
    }
}
