flashpoint_archive/
lib.rs

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