flashpoint_archive/
lib.rs

1use std::{collections::HashMap, sync::{atomic::AtomicBool, mpsc, Arc}};
2use game::{ext::ExtensionInfo, search::{GameFilter, GameSearch, PageTuple, ParsedInput}, AdditionalApp, Game, GameRedirect, PartialGame};
3use game_data::{GameData, PartialGameData};
4use platform::PlatformAppPath;
5use r2d2::Pool;
6use r2d2_sqlite::SqliteConnectionManager;
7use rusqlite::Connection;
8use snafu::ResultExt;
9use tag::{PartialTag, Tag, TagSuggestion};
10use tag_category::{TagCategory, PartialTagCategory};
11use chrono::Utc;
12use lazy_static::lazy_static;
13use crate::logger::EventManager;
14
15mod error;
16use error::{Error, Result};
17use update::{RemoteCategory, RemoteDeletedGamesRes, RemoteGamesRes, RemotePlatform, RemoteTag};
18use util::ContentTreeNode;
19
20pub mod game;
21pub mod game_data;
22mod migration;
23pub mod platform;
24pub mod tag;
25pub mod tag_category;
26pub mod update;
27pub mod util;
28mod logger;
29
30#[cfg(feature = "napi")]
31#[macro_use]
32extern crate napi_derive;
33
34static DEBUG_ENABLED: AtomicBool = AtomicBool::new(false);
35
36pub const MAX_SEARCH: i64 = 99999999999;
37
38lazy_static! {
39    static ref LOGGER: Arc<EventManager> = EventManager::new();
40}
41
42pub struct FlashpointArchive {
43    pool: Option<Pool<SqliteConnectionManager>>,
44    extensions: game::ext::ExtensionRegistry,
45}
46
47impl FlashpointArchive {
48    pub fn new() -> Self {
49        FlashpointArchive {
50            pool: None,
51            extensions: game::ext::ExtensionRegistry::new(),
52        }
53    }
54
55    /// Load a new database for Flashpoint. Open databases will close.
56    /// 
57    /// `source` - Path to database file, or :memory: to open a fresh database in memory
58    pub fn load_database(&mut self, source: &str) -> Result<()> {
59        let conn_manager = if source == ":memory:" {
60            SqliteConnectionManager::memory()
61        } else {
62            SqliteConnectionManager::file(source)
63        };
64
65        let pool = r2d2::Pool::new(conn_manager).expect("Failed to open R2D2 conn pool");
66        let mut conn = pool.get().unwrap();
67
68        // Perform database migrations
69        migration::up(&mut conn).context(error::DatabaseMigrationSnafu)?;
70        conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
71        // Always make there's always a default tag category present 
72        tag_category::find_or_create(&conn, "default", None).context(error::SqliteSnafu)?;
73
74        self.pool = Some(pool);
75
76        Ok(())
77    }
78
79    pub fn parse_user_input(&self, input: &str) -> ParsedInput {
80        game::search::parse_user_input(input, Some(&self.extensions.searchables))
81    }
82
83    pub fn register_extension(&mut self, ext: ExtensionInfo) -> Result<()> {
84        with_transaction!(&self.pool, |tx| {
85            self.extensions.register(tx, ext)
86        })
87    }
88
89    pub async fn search_games(&self, search: &GameSearch) -> Result<Vec<game::Game>> {
90        with_connection!(&self.pool, |conn| {
91            debug_println!("Getting search page");
92            game::search::search(conn, search).context(error::SqliteSnafu)
93        })
94    }
95
96    pub async fn search_games_index(&self, search: &mut GameSearch, limit: Option<i64>) -> Result<Vec<PageTuple>> {
97        with_connection!(&self.pool, |conn| {
98            debug_println!("Getting search index");
99            game::search::search_index(conn, search, limit).context(error::SqliteSnafu)
100        })
101    }
102
103    pub async fn search_games_total(&self, search: &GameSearch) -> Result<i64> {
104        with_connection!(&self.pool, |conn| {
105            debug_println!("Getting search total");
106            game::search::search_count(conn, search).context(error::SqliteSnafu)
107        })
108    }
109
110    pub async fn search_games_with_tag(&self, tag: &str) -> Result<Vec<Game>> {
111        with_connection!(&self.pool, |conn| {
112            game::find_with_tag(conn, tag).context(error::SqliteSnafu)
113        })
114    }
115
116    pub async fn search_games_random(&self, search: &GameSearch, count: i64) -> Result<Vec<Game>> {
117        with_connection!(&self.pool, |conn| {
118            game::search::search_random(conn, search.clone(), count).context(error::SqliteSnafu)
119        })
120    }
121
122    pub async fn search_tag_suggestions(&self, partial: &str, blacklist: Vec<String>) -> Result<Vec<TagSuggestion>> {
123        with_connection!(&self.pool, |conn| {
124            tag::search_tag_suggestions(conn, partial, blacklist).context(error::SqliteSnafu)
125        })
126    }
127
128    pub async fn search_platform_suggestions(&self, partial: &str) -> Result<Vec<TagSuggestion>> {
129        with_connection!(&self.pool, |conn| {
130            platform::search_platform_suggestions(conn, partial).context(error::SqliteSnafu)
131        })
132    }
133
134    pub async fn find_all_game_ids(&self) -> Result<Vec<String>> {
135        with_connection!(&self.pool, |conn| {
136            game::find_all_ids(conn).context(error::SqliteSnafu)
137        })
138    }
139
140    pub async fn find_game(&self, id: &str) -> Result<Option<Game>> {
141        with_connection!(&self.pool, |conn| {
142            game::find(conn, id).context(error::SqliteSnafu)
143        })
144    }
145
146    pub async fn create_game(&self, partial_game: &PartialGame) -> Result<game::Game> {
147        with_transaction!(&self.pool, |tx| {
148            game::create(tx, partial_game).context(error::SqliteSnafu)
149        })
150    }
151
152    pub async fn save_game(&self, partial_game: &mut PartialGame) -> Result<Game> {
153        with_transaction!(&self.pool, |tx| {
154            match partial_game.date_modified {
155                Some(_) => (),
156                None => partial_game.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
157            }
158            game::save(tx, partial_game).context(error::SqliteSnafu)
159        })
160    }
161
162    pub async fn save_games(&self, partial_games: Vec<&mut PartialGame>) -> Result<()> {
163        with_transaction!(&self.pool, |tx| {
164            for partial_game in partial_games {
165                match partial_game.date_modified {
166                    Some(_) => (),
167                    None => partial_game.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
168                }
169                game::save(tx, partial_game).context(error::SqliteSnafu)?;
170            }
171            Ok(())
172        })
173    }
174
175    pub async fn delete_game(&self, id: &str) -> Result<()> {
176        with_transaction!(&self.pool, |conn| {
177            game::delete(conn, id).context(error::SqliteSnafu)
178        })
179    }
180
181    pub async fn count_games(&self) -> Result<i64> {
182        with_connection!(&self.pool, |conn| {
183            game::count(conn).context(error::SqliteSnafu)
184        })
185    }
186
187    pub async fn find_add_app_by_id(&self, id: &str) -> Result<Option<AdditionalApp>> {
188        with_connection!(&self.pool, |conn| {
189            game::find_add_app_by_id(conn, id).context(error::SqliteSnafu)
190        })
191    }
192
193    pub async fn create_add_app(&self, add_app: &mut AdditionalApp) -> Result<()> {
194        with_transaction!(&self.pool, |conn| {
195            game::create_add_app(conn, add_app).context(error::SqliteSnafu)
196        })
197    }
198
199    pub async fn find_game_data_by_id(&self, game_data_id: i64) -> Result<Option<GameData>> {
200        with_connection!(&self.pool, |conn| {
201            game::find_game_data_by_id(conn, game_data_id).context(error::SqliteSnafu)
202        })
203    }
204
205    pub async fn find_game_data(&self, game_id: &str) -> Result<Vec<GameData>> {
206        with_connection!(&self.pool, |conn| {
207            game::get_game_data(conn, game_id).context(error::SqliteSnafu)
208        })
209    }
210
211    pub async fn create_game_data(&self, game_data: &PartialGameData) -> Result<GameData> {
212        with_connection!(&self.pool, |conn| {
213            game::create_game_data(conn, game_data).context(error::SqliteSnafu)
214        })
215    }
216
217    pub async fn save_game_data(&self, game_data: &PartialGameData) -> Result<GameData> {
218        with_connection!(&self.pool, |conn| {
219            game::save_game_data(conn, game_data).context(error::SqliteSnafu)
220        })
221    }
222
223    pub async fn delete_game_data(&self, id: i64) -> Result<()> {
224        with_connection!(&self.pool, |conn| {
225            game_data::delete(conn, id).context(error::SqliteSnafu)
226        })
227    }
228
229    pub async fn find_all_tags(&self) -> Result<Vec<Tag>> {
230        with_connection!(&self.pool, |conn| {
231            tag::find(conn).context(error::SqliteSnafu)
232        })
233    }
234
235    pub async fn find_tag(&self, name: &str) -> Result<Option<Tag>> {
236        with_connection!(&self.pool, |conn| {
237            tag::find_by_name(conn, name).context(error::SqliteSnafu)
238        })
239    }
240
241    pub async fn find_tag_by_id(&self, id: i64) -> Result<Option<Tag>> {
242        with_connection!(&self.pool, |conn| {
243            tag::find_by_id(conn, id).context(error::SqliteSnafu)
244        })
245    }
246
247    pub async fn create_tag(&self, name: &str, category: Option<String>, id: Option<i64>) -> Result<Tag> {
248        with_transaction!(&self.pool, |conn| {
249            tag::create(conn, name, category, id).context(error::SqliteSnafu)
250        })
251    }
252
253    pub async fn save_tag(&self, partial: &mut PartialTag) -> Result<Tag> {
254        with_transaction!(&self.pool, |conn| {
255            match partial.date_modified {
256                Some(_) => (),
257                None => partial.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
258            }
259            tag::save(conn, &partial).context(error::SqliteSnafu)
260        })
261    }
262
263    pub async fn delete_tag(&self, name: &str) -> Result<()> {
264        with_transaction!(&self.pool, |conn| {
265            tag::delete(conn, name).context(error::SqliteSnafu)
266        })
267    }
268
269    pub async fn delete_tag_by_id(&self, id: i64) -> Result<()> {
270        with_transaction!(&self.pool, |conn| {
271            tag::delete_by_id(conn, id).context(error::SqliteSnafu)
272        })
273    }
274
275    pub async fn count_tags(&self) -> Result<i64> {
276        with_connection!(&self.pool, |conn| {
277            tag::count(conn).context(error::SqliteSnafu)
278        })
279    }
280
281    pub async fn merge_tags(&self, name: &str, merged_into: &str) -> Result<Tag> {
282        with_transaction!(&self.pool, |conn| {
283            tag::merge_tag(conn, name, merged_into).context(error::SqliteSnafu)
284        })
285    }
286
287    pub async fn find_all_platforms(&self) -> Result<Vec<Tag>> {
288        with_connection!(&self.pool, |conn| {
289            platform::find(conn).context(error::SqliteSnafu)
290        })
291    }
292
293    pub async fn find_platform(&self, name: &str) -> Result<Option<Tag>> {
294        with_connection!(&self.pool, |conn| {
295            platform::find_by_name(conn, name).context(error::SqliteSnafu)
296        })
297    }
298
299    pub async fn find_platform_by_id(&self, id: i64) -> Result<Option<Tag>> {
300        with_connection!(&self.pool, |conn| {
301            platform::find_by_id(conn, id).context(error::SqliteSnafu)
302        })
303    }
304
305    pub async fn create_platform(&self, name: &str, id: Option<i64>) -> Result<Tag> {
306        with_transaction!(&self.pool, |conn| {
307            platform::create(conn, name, id).context(error::SqliteSnafu)
308        })
309    }
310
311    pub async fn save_platform(&self, partial: &mut PartialTag) -> Result<Tag> {
312        with_transaction!(&self.pool, |conn| {
313            match partial.date_modified {
314                Some(_) => (),
315                None => partial.date_modified = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()),
316            }
317            platform::save(conn, &partial).context(error::SqliteSnafu)
318        })
319    }
320
321    pub async fn delete_platform(&self, name: &str) -> Result<()> {
322        with_transaction!(&self.pool, |conn| {
323            platform::delete(conn, name).context(error::SqliteSnafu)
324        })
325    }
326
327    pub async fn count_platforms(&self) -> Result<i64> {
328        with_connection!(&self.pool, |conn| {
329            platform::count(conn).context(error::SqliteSnafu)
330        })
331    }
332
333    pub async fn find_all_tag_categories(&self) -> Result<Vec<TagCategory>> {
334        with_connection!(&self.pool, |conn| {
335            tag_category::find(conn).context(error::SqliteSnafu)
336        })
337    }
338
339    pub async fn find_tag_category(&self, name: &str) -> Result<Option<TagCategory>> {
340        with_connection!(&self.pool, |conn| {
341            tag_category::find_by_name(conn, name).context(error::SqliteSnafu)
342        })
343    }
344
345    pub async fn find_tag_category_by_id(&self, id: i64) -> Result<Option<TagCategory>> {
346        with_connection!(&self.pool, |conn| {
347            tag_category::find_by_id(conn, id).context(error::SqliteSnafu)
348        })
349    }
350
351    pub async fn create_tag_category(&self, partial: &PartialTagCategory) -> Result<TagCategory> {
352        with_connection!(&self.pool, |conn| {
353            tag_category::create(conn, partial).context(error::SqliteSnafu)
354        })
355    }
356
357    pub async fn save_tag_category(&self, partial: &PartialTagCategory) -> Result<TagCategory> {
358        with_connection!(&self.pool, |conn| {
359            tag_category::save(conn, partial).context(error::SqliteSnafu)
360        })
361    }
362
363    pub async fn new_tag_filter_index(&self, search: &mut GameSearch) -> Result<()> {
364        with_connection!(&self.pool, |conn| {
365            game::search::new_tag_filter_index(conn, search).context(error::SqliteSnafu)
366        })
367    }
368
369    pub async fn find_all_game_developers(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
370        with_connection!(&self.pool, |conn| {
371            game::find_developers(conn, search).context(error::SqliteSnafu)
372        })
373    }
374
375    pub async fn find_all_game_publishers(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
376        with_connection!(&self.pool, |conn| {
377            game::find_publishers(conn, search).context(error::SqliteSnafu)
378        })
379    }
380
381    pub async fn find_all_game_series(&self, search: Option<GameSearch>) -> Result<Vec<String>> {
382        with_connection!(&self.pool, |conn| {
383            game::find_series(conn, search).context(error::SqliteSnafu)
384        })
385    }
386
387    pub async fn find_all_game_libraries(&self) -> Result<Vec<String>> {
388        with_connection!(&self.pool, |conn| {
389            game::find_libraries(conn).context(error::SqliteSnafu)
390        })
391    }
392
393    pub async fn find_all_game_statuses(&self) -> Result<Vec<String>> {
394        with_connection!(&self.pool, |conn| {
395            game::find_statuses(conn).context(error::SqliteSnafu)
396        })
397    }
398
399    pub async fn find_all_game_play_modes(&self) -> Result<Vec<String>> {
400        with_connection!(&self.pool, |conn| {
401            game::find_play_modes(conn).context(error::SqliteSnafu)
402        })
403    }
404
405    pub async fn find_all_game_application_paths(&self) -> Result<Vec<String>> {
406        with_connection!(&self.pool, |conn| {
407            game::find_application_paths(conn).context(error::SqliteSnafu)
408        })
409    }
410
411    pub async fn find_platform_app_paths(&self) -> Result<HashMap<String, Vec<PlatformAppPath>>> {
412        with_connection!(&self.pool, |conn| {
413            game::find_platform_app_paths(conn).context(error::SqliteSnafu)
414        })
415    }
416
417    pub async fn add_game_playtime(&self, game_id: &str, seconds: i64) -> Result<()> {
418        with_transaction!(&self.pool, |conn| {
419            game::add_playtime(conn, game_id, seconds).context(error::SqliteSnafu)
420        })
421    }
422
423    pub async fn clear_playtime_tracking_by_id(&self, game_id: &str) -> Result<()> {
424        with_connection!(&self.pool, |conn| {
425            game::clear_playtime_tracking_by_id(conn, game_id).context(error::SqliteSnafu)
426        })
427    }
428
429    pub async fn clear_playtime_tracking(&self) -> Result<()> {
430        with_connection!(&self.pool, |conn| {
431            game::clear_playtime_tracking(conn).context(error::SqliteSnafu)
432        })
433    }
434
435    pub async fn force_games_active_data_most_recent(&self) -> Result<()> {
436        with_connection!(&self.pool, |conn| {
437            game::force_active_data_most_recent(conn).context(error::SqliteSnafu)
438        })
439    }
440
441    pub async fn find_game_redirects(&self) -> Result<Vec<GameRedirect>> {
442        with_connection!(&self.pool, |conn| {
443            game::find_redirects(conn).context(error::SqliteSnafu)
444        })
445    }
446
447    pub async fn create_game_redirect(&self, src_id: &str, dest_id: &str) -> Result<()> {
448        with_transaction!(&self.pool, |conn| {
449            game::create_redirect(conn, src_id, dest_id).context(error::SqliteSnafu)
450        })
451    }
452
453    pub async fn delete_game_redirect(&self, src_id: &str, dest_id: &str) -> Result<()> {
454        with_transaction!(&self.pool, |conn| {
455            game::delete_redirect(conn, src_id, dest_id).context(error::SqliteSnafu)
456        })
457    }
458
459    pub async fn update_apply_categories(&self, cats: Vec<RemoteCategory>) -> Result<()> {
460        with_transaction!(&self.pool, |conn| {
461            update::apply_categories(conn, cats)
462        })
463    }
464
465    pub async fn update_apply_platforms(&self, platforms: Vec<RemotePlatform>) -> Result<()> {
466        with_transaction!(&self.pool, |conn| {
467            update::apply_platforms(conn, platforms)
468        })
469    }
470    
471    pub async fn update_apply_tags(&self, tags: Vec<RemoteTag>) -> Result<()> {
472        with_transaction!(&self.pool, |conn| {
473            update::apply_tags(conn, tags)
474        })
475    }
476
477    pub async fn update_apply_games(&self, games_res: &RemoteGamesRes) -> Result<()> {
478        with_transaction!(&self.pool, |conn| {
479            update::apply_games(conn, games_res)
480        })
481    }
482
483    pub async fn update_delete_games(&self, games_res: &RemoteDeletedGamesRes) -> Result<()> {
484        with_transaction!(&self.pool, |conn| {
485            update::delete_games(conn, games_res)
486        })
487    }
488
489    pub async fn update_apply_redirects(&self, redirects_res: Vec<GameRedirect>) -> Result<()> {
490        with_transaction!(&self.pool, |conn| {
491            update::apply_redirects(conn, redirects_res)
492        })
493    }
494
495    pub async fn optimize_database(&self) -> Result<()> {
496        with_connection!(&self.pool, |conn| {
497            optimize_database(conn).context(error::SqliteSnafu)
498        })
499    }
500
501    pub async fn new_custom_id_order(&self, custom_id_order: Vec<String>) -> Result<()> {
502        with_transaction!(&self.pool, |conn| {
503            game::search::new_custom_id_order(conn, custom_id_order).context(error::SqliteSnafu)
504        })
505    }
506}
507
508pub fn logger_subscribe() -> (crate::logger::SubscriptionId, mpsc::Receiver<crate::logger::LogEvent>) {
509    LOGGER.subscribe()
510}
511
512pub fn logger_unsubscribe(id: crate::logger::SubscriptionId) {
513    LOGGER.unsubscribe(id)
514}
515
516fn optimize_database(conn: &Connection) -> rusqlite::Result<()> {
517    conn.execute("ANALYZE", ())?;
518    conn.execute("REINDEX", ())?;
519    conn.execute("VACUUM", ())?;
520    Ok(())
521}
522
523pub fn generate_content_tree(root: &str) -> Result<ContentTreeNode> {
524    util::gen_content_tree(root).map_err(|_| snafu::NoneError).context(error::ContentTreeSnafu)
525}
526
527pub fn copy_folder(src: &str, dest: &str) -> Result<u64> {
528    util::copy_folder(src, dest).map_err(|_| snafu::NoneError).context(error::CopyFolderSnafu)
529}
530
531pub fn merge_game_filters(a: &GameFilter, b: &GameFilter) -> GameFilter {
532    let mut new_filter = GameFilter::default();
533    new_filter.subfilters = vec![a.clone(), b.clone()];
534
535    if a.match_any && b.match_any {
536        new_filter.match_any = true;
537    }
538
539    return new_filter;
540}
541
542#[macro_export]
543macro_rules! with_connection {
544    ($pool:expr, $body:expr) => {
545        match $pool {
546            Some(conn) => {
547                let conn = &conn.get().unwrap();
548                conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
549                $body(conn)
550            },
551            None => return Err(Error::DatabaseNotInitialized)
552        }
553    };
554}
555
556#[macro_export]
557macro_rules! with_transaction {
558    ($pool:expr, $body:expr) => {
559        match $pool {
560            Some(conn) => {
561                let mut conn = conn.get().unwrap();
562                conn.execute("PRAGMA foreign_keys=off;", ()).context(error::SqliteSnafu)?;
563                let tx = conn.transaction().context(error::SqliteSnafu)?;
564                let res = $body(&tx);
565                if res.is_ok() {
566                    tx.commit().context(error::SqliteSnafu)?;
567                    debug_println!("Applied transaction");
568                }
569                res
570            },
571            None => return Err(Error::DatabaseNotInitialized)
572        }
573    };
574}
575
576pub fn enable_debug() {
577    DEBUG_ENABLED.store(true, std::sync::atomic::Ordering::SeqCst);
578}
579
580pub fn disable_debug() {
581    DEBUG_ENABLED.store(false, std::sync::atomic::Ordering::SeqCst);
582}
583
584pub fn debug_enabled() -> bool {
585    DEBUG_ENABLED.load(std::sync::atomic::Ordering::SeqCst)
586}
587
588#[macro_export]
589macro_rules! debug_println {
590    ($($arg:tt)*) => (if $crate::debug_enabled() {
591        ::std::println!($($arg)*);
592        let formatted_message = ::std::format!($($arg)*);
593        $crate::LOGGER.dispatch_event(formatted_message);
594    })
595}
596
597#[cfg(test)]
598mod tests {
599
600    use crate::game::{ext::ExtSearchable, search::{parse_user_input, FieldFilter, GameFilter, GameSearchOffset, GameSearchSortable}};
601
602    use super::*;
603
604    const TEST_DATABASE: &str = "benches/flashpoint.sqlite";
605
606    #[tokio::test]
607    async fn database_not_initialized() {
608        let flashpoint = FlashpointArchive::new();
609        let result = flashpoint.count_games().await;
610        assert!(result.is_err());
611
612        let e = result.unwrap_err();
613        assert!(matches!(e, Error::DatabaseNotInitialized {}));
614    }
615
616    #[tokio::test]
617    async fn migrations_valid() {
618        let migrations = migration::get();
619        assert!(migrations.validate().is_ok());
620    }
621
622    #[tokio::test]
623    async fn count_games() {
624        let mut flashpoint = FlashpointArchive::new();
625        let create = flashpoint.load_database(TEST_DATABASE);
626        assert!(create.is_ok());
627        let result = flashpoint.count_games().await;
628        assert!(result.is_ok());
629
630        let total = result.unwrap();
631        assert_eq!(total, 191150);
632    }
633
634    #[tokio::test]
635    async fn search_full_scan() {
636        let mut flashpoint = FlashpointArchive::new();
637        let create = flashpoint.load_database(TEST_DATABASE);
638        assert!(create.is_ok());
639        let mut search = game::search::GameSearch::default();
640        search.limit = MAX_SEARCH;
641        search.filter.exact_whitelist.library = Some(vec![String::from("arcade")]);
642        let result = flashpoint.search_games(&search).await;
643        assert!(result.is_ok());
644        let games = result.unwrap();
645        assert_eq!(games.len(), 162929);
646    }
647
648    #[tokio::test]
649    async fn search_tags_or() {
650        let mut flashpoint = FlashpointArchive::new();
651        let create = flashpoint.load_database(TEST_DATABASE);
652        assert!(create.is_ok());
653        let mut search = game::search::GameSearch::default();
654        search.limit = MAX_SEARCH;
655        search.filter.match_any = true;
656        search.filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
657        let result = flashpoint.search_games(&search).await;
658        assert!(result.is_ok());
659        let games = result.unwrap();
660        assert_eq!(games.len(), 36724);
661    }
662
663    #[tokio::test]
664    async fn search_tags_and() {
665        let mut flashpoint = FlashpointArchive::new();
666        let create = flashpoint.load_database(TEST_DATABASE);
667        assert!(create.is_ok());
668        let mut search = game::search::GameSearch::default();
669        search.limit = MAX_SEARCH;
670        search.filter.match_any = false;
671        search.filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
672        let result = flashpoint.search_games(&search).await;
673        assert!(result.is_ok());
674        let games = result.unwrap();
675        assert_eq!(games.len(), 397);
676    }
677
678    #[tokio::test]
679    async fn search_tags_and_or_combined() {
680        // Has 'Action' or 'Adventure', but is missing 'Sonic The Hedgehog'
681        let mut flashpoint = FlashpointArchive::new();
682        let create = flashpoint.load_database(TEST_DATABASE);
683        assert!(create.is_ok());
684        let mut search = game::search::GameSearch::default();
685        let mut inner_filter = game::search::GameFilter::default();
686        // Set page size for index search
687        search.limit = 30000;
688        // Add the OR to an inner filter
689        inner_filter.exact_whitelist.tags = Some(vec!["Action".to_owned(), "Adventure".to_owned()]);
690        inner_filter.match_any = true; // OR
691        // Add the AND to the main filter, with the inner filter
692        search.filter.subfilters = vec![inner_filter];
693        search.filter.exact_blacklist.tags = Some(vec!["Sonic The Hedgehog".to_owned()]);
694        search.filter.match_any = false; // AND
695        search.order.column = GameSearchSortable::TITLE;
696
697        // Test total results
698        enable_debug();
699        let total_result = flashpoint.search_games_total(&search).await;
700        assert!(total_result.is_ok());
701        let total = total_result.unwrap();
702        assert_eq!(total, 36541);
703
704        // Test first page results
705        let result = flashpoint.search_games(&search).await;
706        assert!(result.is_ok());
707        let games = result.unwrap();
708        assert_eq!(games.len(), 30000);
709        let page_end_game = games.last().unwrap();
710
711        // Test index
712        let index_result = flashpoint.search_games_index(&mut search, None).await;
713        assert!(index_result.is_ok());
714        let index = index_result.unwrap();
715        assert_eq!(index.len(), 1);
716        assert_eq!(index[0].id, page_end_game.id);
717
718        // Test last page results
719        search.offset = Some(GameSearchOffset{
720            value: serde_json::Value::String(page_end_game.title.clone()),
721            game_id: page_end_game.id.clone(),
722            title: page_end_game.title.clone(),
723        });
724        let last_result = flashpoint.search_games(&search).await;
725        assert!(last_result.is_ok());
726        let last_page = last_result.unwrap();
727        assert_eq!(last_page.len(), 6541);
728    }
729
730    #[tokio::test]
731    async fn search_multiple_subfilters() {
732        let mut flashpoint = FlashpointArchive::new();
733        let create = flashpoint.load_database(TEST_DATABASE);
734        assert!(create.is_ok());
735        let mut search = GameSearch::default();
736        search.filter.subfilters.push(GameFilter {
737            exact_blacklist: FieldFilter {
738                tags: Some(vec!["Action".to_owned(), "Shooting".to_owned()]),
739                ..Default::default()
740            },
741            ..Default::default()
742        });
743        search.filter.subfilters.push(GameFilter {
744            exact_blacklist: FieldFilter {
745                tags: Some(vec!["Adventure".to_owned()]),
746                ..Default::default()
747            },
748            ..Default::default()
749        });
750        search.filter.exact_whitelist.library = Some(vec!["arcade".to_owned()]);
751        search.filter.match_any = false;
752        assert!(flashpoint.search_games_index(&mut search, None).await.is_ok());
753    }
754
755    #[tokio::test]
756    async fn parse_user_search_input_assorted() {
757        game::search::parse_user_input("test", None);
758        game::search::parse_user_input(r#"tag:"sonic""#, None);
759        game::search::parse_user_input(r#"o_%$ dev:"san" disk t:7 potato"#, None);
760
761        enable_debug();
762
763        // "" should be treated as exact
764        // Allow key characters in quoted text
765        let s = game::search::parse_user_input(r#"title:"" series:"sonic:hedgehog" -developer:"""#, None).search;
766        assert!(s.filter.exact_whitelist.title.is_some());
767        assert_eq!(s.filter.exact_whitelist.title.unwrap()[0], "");
768        assert!(s.filter.whitelist.series.is_some());
769        assert_eq!(s.filter.whitelist.series.unwrap()[0], "sonic:hedgehog");
770        assert!(s.filter.exact_blacklist.developer.is_some());
771        assert_eq!(s.filter.exact_blacklist.developer.unwrap()[0], "");
772
773        // Make sure the number filters are populated and the time text is processes
774        let s2 = game::search::parse_user_input(r#"playtime>1h30m tags:3 playcount<3"#, None).search;
775        assert!(s2.filter.higher_than.playtime.is_some());
776        assert_eq!(s2.filter.higher_than.playtime.unwrap(), 60 * 90);
777        assert!(s2.filter.equal_to.tags.is_some());
778        assert_eq!(s2.filter.equal_to.tags.unwrap(), 3);
779        assert!(s2.filter.lower_than.playcount.is_some());
780        assert_eq!(s2.filter.lower_than.playcount.unwrap(), 3);
781    }
782
783    #[tokio::test]
784    async fn parse_user_search_input_sizes() {
785        let search = game::search::parse_user_input("tags>5 addapps=3 gamedata<12 test>generic", None).search;
786        assert!(search.filter.higher_than.tags.is_some());
787        assert_eq!(search.filter.higher_than.tags.unwrap(), 5);
788        assert!(search.filter.equal_to.add_apps.is_some());
789        assert_eq!(search.filter.equal_to.add_apps.unwrap(), 3);
790        assert!(search.filter.lower_than.game_data.is_some());
791        assert_eq!(search.filter.lower_than.game_data.unwrap(), 12);
792        assert!(search.filter.whitelist.generic.is_some());
793        let generics = search.filter.whitelist.generic.unwrap();
794        assert_eq!(generics.len(), 1);
795        assert_eq!(generics[0], "test>generic");
796    }
797
798    #[tokio::test]
799    async fn find_game() {
800        let mut flashpoint = FlashpointArchive::new();
801        let create = flashpoint.load_database(TEST_DATABASE);
802        assert!(create.is_ok());
803        let result = flashpoint.find_game("00deff25-5cd2-40d1-a0e7-151d82ce16c5").await;
804        assert!(result.is_ok());
805        let game_opt = result.unwrap();
806        assert!(game_opt.is_some());
807        let game = game_opt.unwrap();
808        assert_eq!(game.title, "Crab Planet");
809        assert!(game.detailed_platforms.is_some());
810        let platforms = game.detailed_platforms.unwrap();
811        assert_eq!(platforms.len(), 1);
812        assert_eq!(platforms[0].name, "Flash");
813    }
814
815    #[tokio::test]
816    async fn game_redirects() {
817        let mut flashpoint = FlashpointArchive::new();
818        let create = flashpoint.load_database(":memory:");
819        assert!(create.is_ok());
820        let partial_game = game::PartialGame {
821            title: Some(String::from("Test Game")),
822            tags: Some(vec!["Action"].into()),
823            ..game::PartialGame::default()
824        };
825        let result = flashpoint.create_game(&partial_game).await;
826        assert!(result.is_ok());
827        let game = result.unwrap();
828
829        let create_redirect_res = flashpoint.create_game_redirect("test", &game.id).await;
830        assert!(create_redirect_res.is_ok());
831
832        // Find game redirect
833        let found_game_res = flashpoint.find_game("test").await;
834        assert!(found_game_res.is_ok());
835        assert!(found_game_res.unwrap().is_some());
836
837        // ID search redirect
838        let mut search = GameSearch::default();
839        search.filter.exact_whitelist.id = Some(vec!["test".to_owned()]);
840        let search_res = flashpoint.search_games(&search).await;
841        assert!(search_res.is_ok());
842        assert_eq!(search_res.unwrap().len(), 1);
843
844        // Find redirects
845        let found_redirs = flashpoint.find_game_redirects().await;
846        assert!(found_redirs.is_ok());
847        assert_eq!(found_redirs.unwrap().len(), 1);
848
849        let remove_redirect_res = flashpoint.delete_game_redirect("test", &game.id).await;
850        assert!(remove_redirect_res.is_ok());
851
852        let found_redirs2 = flashpoint.find_game_redirects().await;
853        assert!(found_redirs2.is_ok());
854        assert_eq!(found_redirs2.unwrap().len(), 0);
855    }
856
857    #[tokio::test]
858    async fn tag_categories() {
859        let mut flashpoint = FlashpointArchive::new();
860        let create = flashpoint.load_database(":memory:");
861        assert!(create.is_ok());
862        let partial_tc = tag_category::PartialTagCategory {
863            id: -1,
864            name: "test".to_owned(),
865            color: "#FF00FF".to_owned(),
866            description: Some("test".to_owned()),
867        };
868        assert!(flashpoint.create_tag_category(&partial_tc).await.is_ok());
869        let saved_cat_result = flashpoint.find_tag_category("test").await;
870        assert!(saved_cat_result.is_ok());
871        let saved_cat_opt = saved_cat_result.unwrap();
872        assert!(saved_cat_opt.is_some());
873        let saved_cat = saved_cat_opt.unwrap();
874        assert_eq!(saved_cat.name, "test");
875        assert_eq!(saved_cat.color, "#FF00FF");
876        assert!(saved_cat.description.is_some());
877        assert_eq!(saved_cat.description.unwrap(), "test");
878
879        let all_cats_result = flashpoint.find_all_tag_categories().await;
880        assert!(all_cats_result.is_ok());
881        let all_cats = all_cats_result.unwrap();
882        // Default category always exists
883        assert_eq!(all_cats.len(), 2);
884    }
885
886    #[tokio::test]
887    async fn create_and_save_game() {
888        let mut flashpoint = FlashpointArchive::new();
889        let create = flashpoint.load_database(":memory:");
890        assert!(create.is_ok());
891        let partial_game = game::PartialGame {
892            title: Some(String::from("Test Game")),
893            tags: Some(vec!["Action"].into()),
894            ..game::PartialGame::default()
895        };
896        let result = flashpoint.create_game(&partial_game).await;
897        assert!(result.is_ok());
898        let mut game = result.unwrap();
899        let found_tag_res = flashpoint.find_tag("Action").await;
900        assert!(found_tag_res.is_ok());
901        let found_tag_opt = found_tag_res.unwrap();
902        assert!(found_tag_opt.is_some());
903        let found_game_res = flashpoint.find_game(&game.id).await;
904        assert!(found_game_res.is_ok());
905        let found_game_opt = found_game_res.unwrap();
906        assert!(found_game_opt.is_some());
907        let found_game = found_game_opt.unwrap();
908        assert!(found_game.detailed_tags.is_some());
909        let found_tags = found_game.detailed_tags.unwrap();
910        assert_eq!(found_tags.len(), 1);
911        assert_eq!(game.title, "Test Game");
912        game.developer = String::from("Newgrounds");
913        game.tags = vec!["Action", "Adventure"].into();
914        game.primary_platform = String::from("Flash");
915        let save_result = flashpoint.save_game(&mut game.into()).await;
916        assert!(save_result.is_ok());
917        let saved_game = save_result.unwrap();
918        assert_eq!(saved_game.developer, "Newgrounds");
919        assert_eq!(saved_game.tags.len(), 2);
920        assert_eq!(saved_game.platforms.len(), 1);
921        assert_eq!(saved_game.platforms[0], "Flash");
922        assert_eq!(saved_game.primary_platform, "Flash");
923        assert!(saved_game.detailed_platforms.is_some());
924        let detailed_platforms = saved_game.detailed_platforms.unwrap();
925        assert_eq!(detailed_platforms.len(), 1);
926        assert!(saved_game.detailed_tags.is_some());
927        let detailed_tags = saved_game.detailed_tags.unwrap();
928        assert_eq!(detailed_tags.len(), 2);
929        assert_eq!(detailed_tags[0].name, "Action");
930    }
931
932    #[tokio::test]
933    async fn game_extension() {
934        let mut flashpoint = FlashpointArchive::new();
935        let create = flashpoint.load_database(":memory:");
936        assert!(create.is_ok());
937        let create_ext = flashpoint.register_extension(ExtensionInfo { 
938            id: "user_score".to_owned(),
939            searchables: vec![ExtSearchable {
940                key: "score".to_owned(),
941                search_key: "score".to_owned(),
942                value_type: game::ext::ExtSearchableType::Number
943            }],
944            indexes: vec![] 
945        });
946        assert!(create_ext.is_ok());
947
948        // Save some game info with ext data
949        let partial_game = game::PartialGame {
950            title: Some(String::from("Test Game")),
951            tags: Some(vec!["Action"].into()),
952            ..game::PartialGame::default()
953        };
954        let game_create_res = flashpoint.create_game(&partial_game).await;
955        assert!(game_create_res.is_ok());
956        let mut game = game_create_res.unwrap();
957        let mut ext_map = HashMap::new();
958        let ext_data = serde_json::from_str(r#"{"score": 5}"#);
959        assert!(ext_data.is_ok());
960        ext_map.insert("user_score".to_owned(), ext_data.unwrap());
961        game.ext_data = Some(ext_map);
962        let save_res = flashpoint.save_game(&mut game.into()).await;
963        assert!(save_res.is_ok());
964
965        // Search for this game
966        let search = parse_user_input("score>3", Some(&flashpoint.extensions.searchables)).search;
967        let search_res = flashpoint.search_games(&search).await;
968        assert!(search_res.is_ok());
969        let res = search_res.unwrap();
970        assert_eq!(res.len(), 1);
971
972        let search = parse_user_input("score<3", Some(&flashpoint.extensions.searchables)).search;
973        let search_res = flashpoint.search_games(&search).await;
974        assert!(search_res.is_ok());
975        let res = search_res.unwrap();
976        assert_eq!(res.len(), 0);
977    }
978
979    #[tokio::test]
980    async fn game_extension_user_input() {
981        let mut flashpoint = FlashpointArchive::new();
982        let create = flashpoint.load_database(":memory:");
983        assert!(create.is_ok());
984        let create_ext = flashpoint.register_extension(ExtensionInfo { 
985            id: "user_score".to_owned(),
986            searchables: vec![
987            ExtSearchable {
988                key: "renamed".to_owned(),
989                search_key: "name".to_owned(),
990                value_type: game::ext::ExtSearchableType::String,
991            },
992            ExtSearchable {
993                key: "fav".to_owned(),
994                search_key: "fav".to_owned(),
995                value_type: game::ext::ExtSearchableType::Boolean,
996            },
997            ExtSearchable {
998                key: "score".to_owned(),
999                search_key: "score".to_owned(),
1000                value_type: game::ext::ExtSearchableType::Number
1001            }],
1002            indexes: vec![],
1003        });
1004        assert!(create_ext.is_ok());
1005        let search = parse_user_input("score>5 name:sonic fav=1", Some(&flashpoint.extensions.searchables)).search;
1006
1007        // Number field
1008        assert!(search.filter.higher_than.ext.is_some());
1009        let ext_search = search.filter.higher_than.ext.unwrap();
1010        assert!(ext_search.contains_key("user_score"));
1011        let ext_search_entry = ext_search.get("user_score").unwrap();
1012        assert!(ext_search_entry.contains_key("score"));
1013        let ext_search_entry_score = ext_search_entry.get("score").unwrap();
1014        assert_eq!(*ext_search_entry_score, 5);
1015
1016        // Bool field
1017        assert!(search.filter.bool_comp.ext.is_some());
1018        let ext_search = search.filter.bool_comp.ext.unwrap();
1019        assert!(ext_search.contains_key("user_score"));
1020        let ext_search_entry = ext_search.get("user_score").unwrap();
1021        assert!(ext_search_entry.contains_key("fav"));
1022        let ext_search_entry_score = ext_search_entry.get("fav").unwrap();
1023        assert_eq!(*ext_search_entry_score, true);
1024
1025        // String field
1026        assert!(search.filter.whitelist.ext.is_some());
1027        let ext_search = search.filter.whitelist.ext.unwrap();
1028        assert!(ext_search.contains_key("user_score"));
1029        let ext_search_entry = ext_search.get("user_score").unwrap();
1030        assert!(ext_search_entry.contains_key("renamed"));
1031        let ext_search_entry_score = ext_search_entry.get("renamed").unwrap();
1032        assert!(ext_search_entry_score.iter().find(|&s| *s == "sonic").is_some());
1033    }
1034
1035    #[tokio::test]
1036    async fn create_and_save_game_data() {
1037        let mut flashpoint = FlashpointArchive::new();
1038        let create = flashpoint.load_database(":memory:");
1039        assert!(create.is_ok());
1040        let partial_game = game::PartialGame {
1041            title: Some(String::from("Test Game")),
1042            tags: Some(vec!["Action"].into()),
1043            ..game::PartialGame::default()
1044        };
1045        let game_create_res = flashpoint.create_game(&partial_game).await;
1046        assert!(game_create_res.is_ok());
1047        let game = game_create_res.unwrap();
1048        let game_data = PartialGameData { 
1049            id: None,
1050            game_id: game.id,
1051            title: Some("Test".to_owned()),
1052            date_added: Some("2023-01-01T01:01:01.000".to_owned()),
1053            sha256: Some("123".to_owned()),
1054            crc32: Some(0),
1055            present_on_disk: Some(false),
1056            path: None,
1057            size: Some(123),
1058            parameters: None,
1059            application_path: Some("Test".to_owned()),
1060            launch_command: Some("Test".to_owned())
1061        };
1062
1063        let game_data_res = flashpoint.create_game_data(&game_data).await;
1064        assert!(game_data_res.is_ok());
1065        let mut gd = game_data_res.unwrap();
1066        gd.path = Some("Test".to_owned());
1067        let save_res = flashpoint.save_game_data(&gd.into()).await;
1068        assert!(save_res.is_ok());
1069        let new_gd = save_res.unwrap();
1070        assert_eq!(new_gd.path.unwrap(), "Test");
1071    }
1072
1073    #[tokio::test]
1074    async fn parse_user_search_input() {
1075        let input = r#"sonic title:"dog cat" -title:"cat dog" tag:Action -mario installed:true"#;
1076        let search = game::search::parse_user_input(input, None).search;
1077        assert!(search.filter.whitelist.generic.is_some());
1078        assert_eq!(search.filter.whitelist.generic.unwrap()[0], "sonic");
1079        assert!(search.filter.whitelist.title.is_some());
1080        assert_eq!(search.filter.whitelist.title.unwrap()[0], "dog cat");
1081        assert!(search.filter.blacklist.title.is_some());
1082        assert_eq!(search.filter.blacklist.title.unwrap()[0], "cat dog");
1083        assert!(search.filter.whitelist.tags.is_some());
1084        assert_eq!(search.filter.whitelist.tags.unwrap()[0], "Action");
1085        assert!(search.filter.blacklist.generic.is_some());
1086        assert_eq!(search.filter.blacklist.generic.unwrap()[0], "mario");
1087        assert!(search.filter.bool_comp.installed.is_some());
1088        assert_eq!(search.filter.bool_comp.installed.unwrap(), true);
1089    }
1090
1091    #[tokio::test]
1092    async fn parse_user_search_input_whitespace() {
1093        let input = r#"series:"紅白Flash合戦  / Red & White Flash Battle 2013""#;
1094        let search = game::search::parse_user_input(input, None).search;
1095        assert!(search.filter.whitelist.series.is_some());
1096        assert_eq!(search.filter.whitelist.series.unwrap()[0], "紅白Flash合戦  / Red & White Flash Battle 2013");
1097    }
1098
1099    #[tokio::test]
1100    async fn parse_user_quick_search_input() {
1101        let input = r#"#Action -!Flash @"armor games" !"#;
1102        let search = game::search::parse_user_input(input, None).search;
1103        assert!(search.filter.whitelist.tags.is_some());
1104        assert_eq!(search.filter.whitelist.tags.unwrap()[0], "Action");
1105        assert!(search.filter.blacklist.platforms.is_some());
1106        assert_eq!(search.filter.blacklist.platforms.unwrap()[0], "Flash");
1107        assert!(search.filter.whitelist.developer.is_some());
1108        assert_eq!(search.filter.whitelist.developer.unwrap()[0], "armor games");
1109        assert!(search.filter.whitelist.generic.is_some());
1110        assert_eq!(search.filter.whitelist.generic.unwrap()[0], "!");
1111    }
1112
1113    #[tokio::test]
1114    async fn parse_user_exact_search_input() {
1115        let input = r#"!Flash -publisher=Newgrounds =sonic"#;
1116        let search = game::search::parse_user_input(input, None).search;
1117        assert!(search.filter.whitelist.platforms.is_some());
1118        assert_eq!(search.filter.whitelist.platforms.unwrap()[0], "Flash");
1119        assert!(search.filter.exact_blacklist.publisher.is_some());
1120        assert_eq!(search.filter.exact_blacklist.publisher.unwrap()[0], "Newgrounds");
1121        assert!(search.filter.whitelist.generic.is_some());
1122        assert!(search.filter.exact_whitelist.generic.is_none());
1123        assert_eq!(search.filter.whitelist.generic.unwrap()[0], "=sonic");
1124    }
1125
1126    #[tokio::test]
1127    async fn find_all_game_libraries() {
1128        let mut flashpoint = FlashpointArchive::new();
1129        let create = flashpoint.load_database(TEST_DATABASE);
1130        assert!(create.is_ok());
1131        let libraries_res = flashpoint.find_all_game_libraries().await;
1132        assert!(libraries_res.is_ok());
1133        let libraries = libraries_res.unwrap();
1134        assert_eq!(libraries.len(), 2);
1135    }
1136
1137    #[tokio::test]
1138    async fn create_tag() {
1139        let mut flashpoint = FlashpointArchive::new();
1140        assert!(flashpoint.load_database(":memory:").is_ok());
1141        let new_tag_res = flashpoint.create_tag("test", None, None).await;
1142        assert!(new_tag_res.is_ok());
1143        let new_tag = new_tag_res.unwrap();
1144        assert!(new_tag.category.is_some());
1145        assert_eq!(new_tag.category.unwrap(), "default");
1146        assert_eq!(new_tag.name, "test");
1147        assert_eq!(new_tag.aliases.len(), 1);
1148        assert_eq!(new_tag.aliases[0], "test");
1149    }
1150
1151    #[tokio::test]
1152    async fn delete_tag() {
1153        let mut flashpoint = FlashpointArchive::new();
1154        assert!(flashpoint.load_database(":memory:").is_ok());
1155        let partial = PartialGame {
1156            title: Some("test".to_owned()),
1157            tags: Some(vec!["Action"].into()),
1158            ..Default::default()
1159        };
1160        let new_game_res = flashpoint.create_game(&partial).await;
1161        assert!(new_game_res.is_ok());
1162        let saved_game = new_game_res.unwrap();
1163        assert_eq!(saved_game.tags.len(), 1);
1164        let delete_res = flashpoint.delete_tag("Action").await;
1165        assert!(delete_res.is_ok());
1166        let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1167        assert!(modded_game_res.is_ok());
1168        let modded_game_opt = modded_game_res.unwrap();
1169        assert!(modded_game_opt.is_some());
1170        let modded_game = modded_game_opt.unwrap();
1171        assert_eq!(modded_game.tags.len(), 0);
1172    }
1173
1174    #[tokio::test]
1175    async fn merge_tags() {
1176        let mut flashpoint = FlashpointArchive::new();
1177        assert!(flashpoint.load_database(":memory:").is_ok());
1178        let partial = PartialGame {
1179            title: Some("test".to_owned()),
1180            tags: Some(vec!["Action"].into()),
1181            ..Default::default()
1182        };
1183        let new_game_res = flashpoint.create_game(&partial).await;
1184        assert!(new_game_res.is_ok());
1185        assert!(flashpoint.create_tag("Adventure", None, None).await.is_ok());
1186        let saved_game = new_game_res.unwrap();
1187        let merged_tag_res = flashpoint.merge_tags("Action", "Adventure").await;
1188        assert!(merged_tag_res.is_ok());
1189        let merged_tag = merged_tag_res.unwrap();
1190        assert_eq!(merged_tag.aliases.len(), 2);
1191        let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1192        assert!(modded_game_res.is_ok());
1193        let modded_game_opt = modded_game_res.unwrap();
1194        assert!(modded_game_opt.is_some());
1195        let modded_game = modded_game_opt.unwrap();
1196        assert_eq!(modded_game.tags.len(), 1);
1197        assert_eq!(modded_game.tags[0], "Adventure");
1198    }
1199
1200    #[tokio::test]
1201    async fn find_tag() {
1202        let mut flashpoint = FlashpointArchive::new();
1203        assert!(flashpoint.load_database(":memory:").is_ok());
1204        let partial = PartialGame {
1205            title: Some("test".to_owned()),
1206            tags: Some(vec!["Action"].into()),
1207            ..Default::default()
1208        };
1209        let new_game_res = flashpoint.create_game(&partial).await;
1210        assert!(new_game_res.is_ok());
1211        let tag_res = flashpoint.find_tag("Action").await;
1212        assert!(tag_res.is_ok());
1213        let tag_opt = tag_res.unwrap();
1214        assert!(tag_opt.is_some());
1215        let tag_id_res = flashpoint.find_tag_by_id(tag_opt.unwrap().id).await;
1216        assert!(tag_id_res.is_ok());
1217        assert!(tag_id_res.unwrap().is_some());
1218    }
1219
1220    #[tokio::test]
1221    async fn delete_platform() {
1222        let mut flashpoint = FlashpointArchive::new();
1223        assert!(flashpoint.load_database(":memory:").is_ok());
1224        let partial = PartialGame {
1225            title: Some("test".to_owned()),
1226            platforms: Some(vec!["Flash"].into()),
1227            ..Default::default()
1228        };
1229        let new_game_res = flashpoint.create_game(&partial).await;
1230        assert!(new_game_res.is_ok());
1231        let saved_game = new_game_res.unwrap();
1232        assert_eq!(saved_game.platforms.len(), 1);
1233        let delete_res = flashpoint.delete_platform("Flash").await;
1234        assert!(delete_res.is_ok());
1235        let modded_game_res = flashpoint.find_game(&saved_game.id).await;
1236        assert!(modded_game_res.is_ok());
1237        let modded_game_opt = modded_game_res.unwrap();
1238        assert!(modded_game_opt.is_some());
1239        let modded_game = modded_game_opt.unwrap();
1240        assert_eq!(modded_game.platforms.len(), 0);
1241    }
1242
1243    #[tokio::test]
1244    async fn create_platform() {
1245        let mut flashpoint = FlashpointArchive::new();
1246        assert!(flashpoint.load_database(":memory:").is_ok());
1247        let new_tag_res = flashpoint.create_platform("test", None).await;
1248        assert!(new_tag_res.is_ok());
1249        let new_tag = new_tag_res.unwrap();
1250        assert!(new_tag.category.is_none());
1251        assert_eq!(new_tag.name, "test");
1252        assert_eq!(new_tag.aliases.len(), 1);
1253        assert_eq!(new_tag.aliases[0], "test");
1254    }
1255
1256    #[tokio::test]
1257    async fn search_tag_suggestions() {
1258        let mut flashpoint = FlashpointArchive::new();
1259        assert!(flashpoint.load_database(":memory:").is_ok());
1260        let new_tag_res = flashpoint.create_tag("Action", None, None).await;
1261        assert!(new_tag_res.is_ok());
1262        let suggs_res = flashpoint.search_tag_suggestions("Act", vec![]).await;
1263        assert!(suggs_res.is_ok());
1264        assert_eq!(suggs_res.unwrap().len(), 1);
1265        let suggs_bad_res = flashpoint.search_tag_suggestions("Adventure", vec![]).await;
1266        assert!(suggs_bad_res.is_ok());
1267        assert_eq!(suggs_bad_res.unwrap().len(), 0);
1268    }
1269
1270    #[tokio::test]
1271    async fn update_game_when_platform_changed() {
1272        let mut flashpoint = FlashpointArchive::new();
1273        assert!(flashpoint.load_database(":memory:").is_ok());
1274        let partial_game = game::PartialGame {
1275            title: Some(String::from("Test Game")),
1276            tags: Some(vec!["Action"].into()),
1277            platforms: Some(vec!["Flash", "HTML5"].into()),
1278            primary_platform: Some("HTML5".into()),
1279            ..game::PartialGame::default()
1280        };
1281        let result = flashpoint.create_game(&partial_game).await;
1282        assert!(result.is_ok());
1283        let old_game = result.unwrap();
1284        let mut platform = flashpoint.find_platform("HTML5").await.unwrap().unwrap();
1285        platform.name = String::from("Wiggle");
1286        let mut partial = PartialTag::from(platform);
1287        let save_res = flashpoint.save_platform(&mut partial).await;
1288        assert!(save_res.is_ok());
1289        assert_eq!(save_res.unwrap().name, "Wiggle");
1290        let new_game = flashpoint.find_game(&old_game.id).await.unwrap().unwrap();
1291        assert_eq!(new_game.primary_platform, "Wiggle");
1292        assert!(new_game.platforms.contains(&"Wiggle".to_string()));
1293    }
1294
1295    #[tokio::test]
1296    async fn search_games_random() {
1297        let mut flashpoint = FlashpointArchive::new();
1298        let create = flashpoint.load_database(TEST_DATABASE);
1299        assert!(create.is_ok());
1300
1301        let mut search = crate::game::search::parse_user_input("", None).search;
1302        let mut new_filter = GameFilter::default();
1303        new_filter.exact_blacklist.tags = Some(vec!["Action".to_owned()]);
1304        search.filter.subfilters.push(new_filter);
1305
1306        let random_res = flashpoint.search_games_random(&search, 5).await;
1307        assert!(random_res.is_ok());
1308        assert_eq!(random_res.unwrap().len(), 5);
1309    }
1310
1311    #[tokio::test]
1312    async fn search_games_installed() {
1313        let mut flashpoint = FlashpointArchive::new();
1314        let create = flashpoint.load_database(TEST_DATABASE);
1315        assert!(create.is_ok());
1316
1317        let mut search = crate::game::search::parse_user_input("installed:true", None).search;
1318        if let Some(installed) = search.filter.bool_comp.installed.as_ref() {
1319            assert_eq!(installed, &true);
1320        } else {
1321            panic!("Expected 'installed' to be Some(true), but it was None.");
1322        }
1323
1324        search.limit = 200;
1325        let games_res = flashpoint.search_games(&search).await;
1326        assert!(games_res.is_ok());
1327        assert_eq!(games_res.unwrap().len(), 20);
1328    }
1329
1330    #[tokio::test]
1331    async fn search_games_index_limited() {
1332        let mut flashpoint = FlashpointArchive::new();
1333        let create = flashpoint.load_database(TEST_DATABASE);
1334        assert!(create.is_ok());
1335
1336        let search = &mut GameSearch::default();
1337        search.filter.whitelist.title = Some(vec!["Super".into()]);
1338        // Set page size
1339        search.limit = 200;
1340        let index_res = flashpoint.search_games_index(&mut search.clone(), Some(1000)).await;
1341        assert!(index_res.is_ok());
1342        let index = index_res.unwrap();
1343        assert_eq!(index.len(), 5);
1344    }
1345
1346    
1347    #[tokio::test]
1348    async fn search_bracketting() {
1349        let mut flashpoint = FlashpointArchive::new();
1350        let create = flashpoint.load_database(TEST_DATABASE);
1351        assert!(create.is_ok());
1352
1353        let search = &mut GameSearch::default();
1354
1355        let mut tag_filter = GameFilter::default();
1356        tag_filter.whitelist.tags = Some(vec!["Alien Hominid".into()]);
1357
1358        let mut dev_filter = GameFilter::default();
1359        dev_filter.whitelist.developer = Some(vec!["jmtb".into(), "Tom Fulp".into()]);
1360        dev_filter.match_any = true;
1361
1362        search.filter.match_any = false;
1363        search.filter.subfilters.push(dev_filter);
1364        search.filter.subfilters.push(tag_filter);
1365
1366        let games_res = flashpoint.search_games(&search).await;
1367        assert!(games_res.is_ok());
1368        assert_eq!(games_res.unwrap().len(), 1);
1369    }
1370
1371    #[tokio::test]
1372    async fn get_tag() {
1373        let mut flashpoint = FlashpointArchive::new();
1374        let create = flashpoint.load_database(TEST_DATABASE);
1375        assert!(create.is_ok());
1376
1377        let tag_res = flashpoint.find_tag("Mario Bros.").await;
1378        assert!(tag_res.is_ok());
1379        let tag = tag_res.unwrap();
1380        assert!(tag.is_some());
1381        assert_eq!(tag.unwrap().name, "Super Mario");
1382    }
1383
1384    #[tokio::test]
1385    async fn get_platform() {
1386        let mut flashpoint = FlashpointArchive::new();
1387        let create = flashpoint.load_database(TEST_DATABASE);
1388        assert!(create.is_ok());
1389
1390        let tag_res = flashpoint.find_platform("Jutvision").await;
1391        assert!(tag_res.is_ok());
1392        let tag = tag_res.unwrap();
1393        assert!(tag.is_some());
1394        assert_eq!(tag.unwrap().name, "asdadawdaw");
1395    }
1396
1397    #[tokio::test]
1398    async fn add_playtime() {
1399        let mut flashpoint = FlashpointArchive::new();
1400        let create = flashpoint.load_database(":memory:");
1401        assert!(create.is_ok());
1402        let partial_game = game::PartialGame {
1403            title: Some(String::from("Test Game")),
1404            tags: Some(vec!["Action"].into()),
1405            ..game::PartialGame::default()
1406        };
1407        let result = flashpoint.create_game(&partial_game).await;
1408        assert!(result.is_ok());
1409        let game_id = result.unwrap().id;
1410        let playtime_res = flashpoint.add_game_playtime(&game_id, 30).await;
1411        assert!(playtime_res.is_ok());
1412        let saved_game_res = flashpoint.find_game(&game_id).await;
1413        assert!(saved_game_res.is_ok());
1414        let saved_game_opt = saved_game_res.unwrap();
1415        assert!(saved_game_opt.is_some());
1416        let saved_game = saved_game_opt.unwrap();
1417        assert_eq!(saved_game.playtime, 30);
1418        assert_eq!(saved_game.play_counter, 1);
1419    }
1420
1421    #[tokio::test]
1422    async fn update_tags_clear_existing() {
1423        let mut flashpoint = FlashpointArchive::new();
1424        let create = flashpoint.load_database(":memory:");
1425        assert!(create.is_ok());
1426        let new_tag_res = flashpoint.create_tag("test", None, Some(10)).await;
1427        assert!(new_tag_res.is_ok());
1428        let tag_update = RemoteTag {
1429            id: 10,
1430            name: "hello".to_owned(),
1431            description: String::new(),
1432            category: "default".to_owned(),
1433            date_modified: "2024-01-01 12:00:00".to_owned(),
1434            aliases: vec!["hello".to_owned()],
1435            deleted: false,
1436        };
1437        let update_res = flashpoint.update_apply_tags(vec![tag_update]).await;
1438        assert!(update_res.is_ok());
1439        let saved_tag_res = flashpoint.find_tag_by_id(10).await;
1440        assert!(saved_tag_res.is_ok());
1441        let saved_tag_opt = saved_tag_res.unwrap();
1442        assert!(saved_tag_opt.is_some());
1443        let saved_tag = saved_tag_opt.unwrap();
1444        assert_eq!(saved_tag.aliases.len(), 1);
1445        assert_eq!(saved_tag.aliases[0].as_str(), "hello");
1446        assert_eq!(saved_tag.name.as_str(), "hello");
1447    }
1448}