arct-core 1.1.5

Core library for Arc Academy Terminal - command analysis and learning engine
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
//! Daily and weekly challenge system
//!
//! This module provides an engaging challenge system with daily tasks,
//! weekly scenarios, and speed challenges to motivate continuous learning.

use crate::lesson::{CommandValidation, Difficulty, LessonValidator};
use crate::stats::UserStats;
use chrono::{Datelike, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;

/// Represents a learning challenge that can be completed
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Challenge {
    pub id: String,
    pub title: String,
    pub description: String,
    pub difficulty: Difficulty,
    pub challenge_type: ChallengeType,
    pub points: u32,
}

/// Different types of challenges available
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ChallengeType {
    /// A single command task to complete daily
    DailyCommand {
        command: String,
        task_description: String,
        validation: CommandValidation,
    },
    /// Multi-step scenario challenge for weekly engagement
    WeeklyScenario {
        scenario: String,
        steps: Vec<ChallengeStep>,
    },
    /// Timed challenge to complete tasks quickly
    SpeedChallenge {
        task: String,
        time_limit_seconds: u32,
        commands: Vec<String>,
    },
}

/// A single step within a multi-step challenge
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChallengeStep {
    pub description: String,
    pub validation: CommandValidation,
    pub hint: Option<String>,
}

/// Manages active and completed challenges
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChallengeManager {
    /// Currently active daily challenge
    pub daily_challenge: Option<Challenge>,
    /// Currently active weekly challenge
    pub weekly_challenge: Option<Challenge>,
    /// Set of completed challenge IDs
    pub completed_challenges: HashSet<String>,
    /// Date when daily challenge was last generated
    last_daily_generated: Option<chrono::NaiveDate>,
    /// ISO week number when weekly challenge was last generated
    last_weekly_generated: Option<u32>,
    /// Progress (completed step count) through the active weekly scenario
    #[serde(default)]
    weekly_step_progress: usize,
}

impl ChallengeManager {
    /// Create a new challenge manager
    pub fn new() -> Self {
        Self {
            daily_challenge: None,
            weekly_challenge: None,
            completed_challenges: HashSet::new(),
            last_daily_generated: None,
            last_weekly_generated: None,
            weekly_step_progress: 0,
        }
    }

    /// Get or generate today's daily challenge
    pub fn get_daily_challenge(&mut self) -> Challenge {
        let today = Utc::now().date_naive();

        // Check if we need to generate a new daily challenge
        if self.last_daily_generated != Some(today) {
            self.daily_challenge = Some(self.generate_daily_challenge(today));
            self.last_daily_generated = Some(today);
        }

        self.daily_challenge.clone().unwrap()
    }

    /// Get or generate this week's weekly challenge
    pub fn get_weekly_challenge(&mut self) -> Challenge {
        let now = Utc::now();
        let current_week = now.iso_week().week();

        // Check if we need to generate a new weekly challenge
        if self.last_weekly_generated != Some(current_week) {
            self.weekly_challenge = Some(self.generate_weekly_challenge(current_week));
            self.last_weekly_generated = Some(current_week);
            self.weekly_step_progress = 0;
        }

        self.weekly_challenge.clone().unwrap()
    }

    /// Mark a challenge as completed
    ///
    /// Note: this only updates the manager's own bookkeeping (used by
    /// `is_daily_completed` / `is_weekly_completed` UI helpers). For
    /// achievement purposes `UserStats` is the single source of truth —
    /// prefer `check_command`, which keeps both in sync.
    pub fn complete_challenge(&mut self, challenge_id: String) {
        self.completed_challenges.insert(challenge_id);
    }

    /// Validation entry point the TUI should call after every executed shell
    /// command.
    ///
    /// Checks the command against the currently active daily and weekly
    /// challenges (as previously generated by `get_daily_challenge` /
    /// `get_weekly_challenge`), advancing multi-step weekly-scenario
    /// progress as steps are matched. When a challenge is completed, the
    /// completion is recorded in this manager AND routed into `stats`
    /// (`UserStats::complete_challenge` + the `"first_challenge"` event), so
    /// UserStats remains the single source of truth for achievements.
    ///
    /// Returns the challenge the command just completed, if any. Call
    /// `stats.check_achievements()` afterwards (or use the achievements
    /// already returned by the routed `record_event`) to surface unlocks.
    pub fn check_command(&mut self, command: &str, stats: &mut UserStats) -> Option<Challenge> {
        let completed = self.match_command(command)?;

        self.complete_challenge(completed.id.clone());
        stats.complete_challenge(completed.id.clone());
        stats.record_event("first_challenge");

        Some(completed)
    }

    /// Find the active, not-yet-completed challenge that `command` satisfies,
    /// advancing weekly-scenario step progress as a side effect.
    fn match_command(&mut self, command: &str) -> Option<Challenge> {
        let validator = LessonValidator::new();

        // Daily challenge: a single matching command completes it
        if let Some(daily) = &self.daily_challenge {
            if !self.is_challenge_completed(&daily.id)
                && Self::command_satisfies(&validator, daily, command)
            {
                return Some(daily.clone());
            }
        }

        // Weekly scenario: commands must satisfy each step in order
        if let Some(weekly) = self.weekly_challenge.clone() {
            if !self.is_challenge_completed(&weekly.id) {
                if let ChallengeType::WeeklyScenario { steps, .. } = &weekly.challenge_type {
                    if let Some(step) = steps.get(self.weekly_step_progress) {
                        // Scenario steps carry only a validation (typically
                        // Regex), so there is no expected command string.
                        if validator
                            .validate_command(command, "", &step.validation)
                            .is_success()
                        {
                            self.weekly_step_progress += 1;
                            if self.weekly_step_progress >= steps.len() {
                                return Some(weekly);
                            }
                        }
                    }
                }
            }
        }

        None
    }

    /// Check whether a single command satisfies a single-command challenge
    fn command_satisfies(validator: &LessonValidator, challenge: &Challenge, command: &str) -> bool {
        match &challenge.challenge_type {
            ChallengeType::DailyCommand {
                command: expected,
                validation,
                ..
            } => validator.validate_command(command, expected, validation).is_success(),
            ChallengeType::SpeedChallenge { commands, .. } => {
                let normalize = |s: &str| s.split_whitespace().collect::<Vec<_>>().join(" ");
                let input = normalize(command);
                commands.iter().any(|c| normalize(c) == input)
            }
            // Multi-step scenarios are handled step-by-step in match_command
            ChallengeType::WeeklyScenario { .. } => false,
        }
    }

    /// Check if a challenge has been completed
    pub fn is_challenge_completed(&self, challenge_id: &str) -> bool {
        self.completed_challenges.contains(challenge_id)
    }

    /// Generate a daily challenge based on the date
    fn generate_daily_challenge(&self, date: chrono::NaiveDate) -> Challenge {
        let all_daily = all_daily_challenges();
        // Use day of year to deterministically select a challenge
        let index = (date.ordinal0() as usize) % all_daily.len();
        all_daily[index].clone()
    }

    /// Generate a weekly challenge based on the week number
    fn generate_weekly_challenge(&self, week: u32) -> Challenge {
        let all_weekly = all_weekly_challenges();
        // Use week number to deterministically select a challenge
        let index = (week as usize - 1) % all_weekly.len();
        all_weekly[index].clone()
    }

    /// Get all challenges completed this week
    pub fn weekly_completions(&self) -> usize {
        // This could be enhanced to track completion timestamps
        // For now, returns total completed challenges
        self.completed_challenges.len()
    }

    /// Check if today's daily challenge is completed
    pub fn is_daily_completed(&self) -> bool {
        if let Some(ref daily) = self.daily_challenge {
            self.is_challenge_completed(&daily.id)
        } else {
            false
        }
    }

    /// Check if this week's weekly challenge is completed
    pub fn is_weekly_completed(&self) -> bool {
        if let Some(ref weekly) = self.weekly_challenge {
            self.is_challenge_completed(&weekly.id)
        } else {
            false
        }
    }
}

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

/// Get all available daily challenges
pub fn all_daily_challenges() -> Vec<Challenge> {
    vec![
        Challenge {
            id: "daily_grep_recursive".to_string(),
            title: "Master Recursive Search".to_string(),
            description: "Use grep with the -r flag to search for 'error' in all files within a directory.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "grep -r 'error' /var/log".to_string(),
                task_description: "Search recursively for the word 'error' in log files".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 25,
        },
        Challenge {
            id: "daily_chmod_executable".to_string(),
            title: "Make It Executable".to_string(),
            description: "Use chmod to make a script file executable by adding execute permissions.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "chmod +x script.sh".to_string(),
                task_description: "Make a file executable using chmod".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 20,
        },
        Challenge {
            id: "daily_find_by_name".to_string(),
            title: "Find by Name".to_string(),
            description: "Use the find command to locate all .txt files in your home directory.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "find ~ -name '*.txt'".to_string(),
                task_description: "Find all text files in home directory".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 25,
        },
        Challenge {
            id: "daily_pipe_mastery".to_string(),
            title: "Pipe Master".to_string(),
            description: "Combine ls and grep using a pipe to find files containing 'test' in their names.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "ls | grep test".to_string(),
                task_description: "Use pipes to filter ls output with grep".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 20,
        },
        Challenge {
            id: "daily_tar_archive".to_string(),
            title: "Archive Creation".to_string(),
            description: "Create a compressed tar archive of a directory using tar -czf.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "tar -czf archive.tar.gz directory/".to_string(),
                task_description: "Create a compressed tar archive".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 30,
        },
        Challenge {
            id: "daily_curl_download".to_string(),
            title: "Web Fetcher".to_string(),
            description: "Use curl to download a file from a URL and save it with -o option.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "curl -o output.html https://example.com".to_string(),
                task_description: "Download a file using curl".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 25,
        },
        Challenge {
            id: "daily_process_check".to_string(),
            title: "Process Inspector".to_string(),
            description: "Use ps aux combined with grep to find a specific running process.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "ps aux | grep firefox".to_string(),
                task_description: "Find a running process by name".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 25,
        },
        Challenge {
            id: "daily_disk_usage".to_string(),
            title: "Disk Usage Detective".to_string(),
            description: "Use du -sh to find the size of a directory in human-readable format.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "du -sh /var/log".to_string(),
                task_description: "Check directory size with du".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 20,
        },
        Challenge {
            id: "daily_sed_replace".to_string(),
            title: "Text Transformer".to_string(),
            description: "Use sed to replace all occurrences of a word in a file.".to_string(),
            difficulty: Difficulty::Advanced,
            challenge_type: ChallengeType::DailyCommand {
                command: "sed -i 's/old/new/g' file.txt".to_string(),
                task_description: "Replace text in a file using sed".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 35,
        },
        Challenge {
            id: "daily_symlink_create".to_string(),
            title: "Link Creator".to_string(),
            description: "Create a symbolic link using ln -s pointing to a target file.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "ln -s /path/to/target linkname".to_string(),
                task_description: "Create a symbolic link".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 20,
        },
        Challenge {
            id: "daily_history_search".to_string(),
            title: "History Hunter".to_string(),
            description: "Use history with grep to find commands you've run in the past.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "history | grep git".to_string(),
                task_description: "Search command history".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 15,
        },
        Challenge {
            id: "daily_wc_count".to_string(),
            title: "Word Counter".to_string(),
            description: "Count the number of lines in a file using wc -l.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "wc -l file.txt".to_string(),
                task_description: "Count lines in a file".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 15,
        },
        Challenge {
            id: "daily_env_check".to_string(),
            title: "Environment Explorer".to_string(),
            description: "Use env or printenv to display all environment variables.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::DailyCommand {
                command: "env".to_string(),
                task_description: "Display environment variables".to_string(),
                validation: CommandValidation::CommandOnly,
            },
            points: 15,
        },
        Challenge {
            id: "daily_tail_follow".to_string(),
            title: "Log Follower".to_string(),
            description: "Use tail -f to continuously monitor a log file in real-time.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::DailyCommand {
                command: "tail -f /var/log/syslog".to_string(),
                task_description: "Follow a log file in real-time".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 25,
        },
        Challenge {
            id: "daily_awk_column".to_string(),
            title: "Column Extractor".to_string(),
            description: "Use awk to extract a specific column from formatted text output.".to_string(),
            difficulty: Difficulty::Advanced,
            challenge_type: ChallengeType::DailyCommand {
                command: "ps aux | awk '{print $1, $11}'".to_string(),
                task_description: "Extract specific columns using awk".to_string(),
                validation: CommandValidation::CommandAndFlags,
            },
            points: 35,
        },
    ]
}

/// Get all available weekly challenges
pub fn all_weekly_challenges() -> Vec<Challenge> {
    vec![
        Challenge {
            id: "weekly_debug_server".to_string(),
            title: "Debug the Broken Server".to_string(),
            description: "A simulated web server isn't starting. Use diagnostic commands to find and fix the issues.".to_string(),
            difficulty: Difficulty::Advanced,
            challenge_type: ChallengeType::WeeklyScenario {
                scenario: "The web server on port 8080 isn't responding. Debug the issue step by step.".to_string(),
                steps: vec![
                    ChallengeStep {
                        description: "Check if the server process is running".to_string(),
                        validation: CommandValidation::Regex("ps.*8080|netstat.*8080|lsof.*8080".to_string()),
                        hint: Some("Try 'ps aux | grep server' or 'netstat -tulpn'".to_string()),
                    },
                    ChallengeStep {
                        description: "Check the server log files for errors".to_string(),
                        validation: CommandValidation::Regex("tail.*log|cat.*log|less.*log".to_string()),
                        hint: Some("Look in /var/log/ or check application-specific logs".to_string()),
                    },
                    ChallengeStep {
                        description: "Verify the port is not in use by another process".to_string(),
                        validation: CommandValidation::Regex("netstat.*8080|lsof.*8080|ss.*8080".to_string()),
                        hint: Some("Use 'lsof -i :8080' or 'netstat -tulpn | grep 8080'".to_string()),
                    },
                ],
            },
            points: 100,
        },
        Challenge {
            id: "weekly_organize_files".to_string(),
            title: "Organize the Chaos".to_string(),
            description: "A directory is filled with mixed file types. Organize them into proper subdirectories.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::WeeklyScenario {
                scenario: "The downloads folder has 100+ files of different types all mixed together. Organize them!".to_string(),
                steps: vec![
                    ChallengeStep {
                        description: "Create directories for different file types (images, documents, code)".to_string(),
                        validation: CommandValidation::Regex("mkdir.*images|mkdir.*documents|mkdir.*code".to_string()),
                        hint: Some("Use 'mkdir -p' to create multiple directories".to_string()),
                    },
                    ChallengeStep {
                        description: "Find and move all image files to the images directory".to_string(),
                        validation: CommandValidation::Regex("mv.*\\.(jpg|png|gif)|find.*\\.(jpg|png).*-exec mv".to_string()),
                        hint: Some("Use 'find' with '-name' or a for loop with 'mv'".to_string()),
                    },
                    ChallengeStep {
                        description: "Create an archive of the organized directories".to_string(),
                        validation: CommandValidation::Regex("tar.*czf|zip.*-r".to_string()),
                        hint: Some("Use 'tar -czf organized.tar.gz' or 'zip -r'".to_string()),
                    },
                ],
            },
            points: 80,
        },
        Challenge {
            id: "weekly_security_audit".to_string(),
            title: "Security Audit".to_string(),
            description: "Perform a basic security audit by checking file permissions and user access.".to_string(),
            difficulty: Difficulty::Advanced,
            challenge_type: ChallengeType::WeeklyScenario {
                scenario: "Audit file permissions and find potential security issues in a system directory.".to_string(),
                steps: vec![
                    ChallengeStep {
                        description: "Find all world-writable files".to_string(),
                        validation: CommandValidation::Regex("find.*-perm.*777|find.*-perm.*o=w".to_string()),
                        hint: Some("Use 'find /path -perm -002'".to_string()),
                    },
                    ChallengeStep {
                        description: "List all users who can access the system".to_string(),
                        validation: CommandValidation::Regex("cat /etc/passwd|getent passwd|cut.*passwd".to_string()),
                        hint: Some("Check /etc/passwd or use 'getent passwd'".to_string()),
                    },
                    ChallengeStep {
                        description: "Check for files with setuid bit enabled".to_string(),
                        validation: CommandValidation::Regex("find.*-perm.*4000|find.*-perm.*u=s".to_string()),
                        hint: Some("Use 'find / -perm -4000'".to_string()),
                    },
                ],
            },
            points: 120,
        },
        Challenge {
            id: "weekly_backup_strategy".to_string(),
            title: "Backup Strategy".to_string(),
            description: "Create a comprehensive backup of important files with proper organization.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::WeeklyScenario {
                scenario: "Set up an automated backup system for critical directories.".to_string(),
                steps: vec![
                    ChallengeStep {
                        description: "Create a timestamped backup directory".to_string(),
                        validation: CommandValidation::Regex("mkdir.*backup.*$(date|date.*\\+)".to_string()),
                        hint: Some("Use 'mkdir backup-$(date +%Y%m%d)'".to_string()),
                    },
                    ChallengeStep {
                        description: "Copy important files preserving permissions and timestamps".to_string(),
                        validation: CommandValidation::Regex("cp -a|cp.*-p|rsync.*-a".to_string()),
                        hint: Some("Use 'cp -a' or 'rsync -av'".to_string()),
                    },
                    ChallengeStep {
                        description: "Create a compressed archive of the backup".to_string(),
                        validation: CommandValidation::Regex("tar.*czf|gzip|zip".to_string()),
                        hint: Some("Use 'tar -czf backup.tar.gz backup/'".to_string()),
                    },
                ],
            },
            points: 90,
        },
        Challenge {
            id: "weekly_log_analysis".to_string(),
            title: "Log Analysis Detective".to_string(),
            description: "Analyze system logs to find errors, warnings, and unusual patterns.".to_string(),
            difficulty: Difficulty::Advanced,
            challenge_type: ChallengeType::WeeklyScenario {
                scenario: "Investigate system logs to identify and report issues.".to_string(),
                steps: vec![
                    ChallengeStep {
                        description: "Find all error messages in system logs".to_string(),
                        validation: CommandValidation::Regex("grep.*error|grep.*ERROR|egrep.*'error|ERROR'".to_string()),
                        hint: Some("Use 'grep -i error /var/log/syslog'".to_string()),
                    },
                    ChallengeStep {
                        description: "Count occurrences of different error types".to_string(),
                        validation: CommandValidation::Regex("sort|uniq -c|wc".to_string()),
                        hint: Some("Combine 'sort | uniq -c | sort -n'".to_string()),
                    },
                    ChallengeStep {
                        description: "Extract and save the top 10 most frequent errors to a file".to_string(),
                        validation: CommandValidation::Regex("head.*>|>.*head|tee".to_string()),
                        hint: Some("Use pipes and redirect with '> errors.txt'".to_string()),
                    },
                ],
            },
            points: 110,
        },
    ]
}

/// Get all available speed challenges
pub fn all_speed_challenges() -> Vec<Challenge> {
    vec![
        Challenge {
            id: "speed_cleanup_tmp".to_string(),
            title: "Rapid Cleanup".to_string(),
            description: "Find and delete all .tmp files in a directory tree within 60 seconds.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::SpeedChallenge {
                task: "Remove all temporary files (.tmp extension) from the current directory and subdirectories".to_string(),
                time_limit_seconds: 60,
                commands: vec![
                    "find . -name '*.tmp' -delete".to_string(),
                    "find . -name '*.tmp' -exec rm {} \\;".to_string(),
                ],
            },
            points: 50,
        },
        Challenge {
            id: "speed_count_files".to_string(),
            title: "Quick Count".to_string(),
            description: "Count all files in a directory within 30 seconds.".to_string(),
            difficulty: Difficulty::Beginner,
            challenge_type: ChallengeType::SpeedChallenge {
                task: "Count the total number of files (not directories) in the current directory tree".to_string(),
                time_limit_seconds: 30,
                commands: vec![
                    "find . -type f | wc -l".to_string(),
                    "ls -lR | grep '^-' | wc -l".to_string(),
                ],
            },
            points: 40,
        },
        Challenge {
            id: "speed_largest_files".to_string(),
            title: "Size Hunter".to_string(),
            description: "Find the 5 largest files in a directory within 45 seconds.".to_string(),
            difficulty: Difficulty::Intermediate,
            challenge_type: ChallengeType::SpeedChallenge {
                task: "List the 5 largest files in the directory tree with their sizes".to_string(),
                time_limit_seconds: 45,
                commands: vec![
                    "find . -type f -exec ls -lh {} \\; | sort -k5 -hr | head -5".to_string(),
                    "du -ah . | sort -rh | head -5".to_string(),
                ],
            },
            points: 55,
        },
    ]
}

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

    #[test]
    fn test_challenge_manager_creation() {
        let manager = ChallengeManager::new();
        assert!(manager.daily_challenge.is_none());
        assert!(manager.weekly_challenge.is_none());
        assert_eq!(manager.completed_challenges.len(), 0);
    }

    #[test]
    fn test_daily_challenge_generation() {
        let mut manager = ChallengeManager::new();
        let challenge = manager.get_daily_challenge();
        assert!(!challenge.id.is_empty());
        assert!(!challenge.title.is_empty());
        assert!(challenge.points > 0);
    }

    #[test]
    fn test_weekly_challenge_generation() {
        let mut manager = ChallengeManager::new();
        let challenge = manager.get_weekly_challenge();
        assert!(!challenge.id.is_empty());
        assert!(!challenge.title.is_empty());
        assert!(challenge.points > 0);
    }

    #[test]
    fn test_challenge_completion() {
        let mut manager = ChallengeManager::new();
        assert!(!manager.is_challenge_completed("test_challenge"));

        manager.complete_challenge("test_challenge".to_string());
        assert!(manager.is_challenge_completed("test_challenge"));
    }

    #[test]
    fn test_daily_challenges_exist() {
        let challenges = all_daily_challenges();
        assert!(!challenges.is_empty());
        assert!(challenges.len() >= 10);
    }

    #[test]
    fn test_weekly_challenges_exist() {
        let challenges = all_weekly_challenges();
        assert!(!challenges.is_empty());
        assert!(challenges.len() >= 5);
    }

    #[test]
    fn test_deterministic_daily_selection() {
        let mut manager1 = ChallengeManager::new();
        let mut manager2 = ChallengeManager::new();

        let challenge1 = manager1.get_daily_challenge();
        let challenge2 = manager2.get_daily_challenge();

        // Same day should give same challenge
        assert_eq!(challenge1.id, challenge2.id);
    }

    #[test]
    fn test_check_command_completes_daily_challenge() {
        let mut manager = ChallengeManager::new();
        let mut stats = UserStats::new();
        let daily = manager.get_daily_challenge();

        // A non-matching command completes nothing
        assert!(manager.check_command("echo hello", &mut stats).is_none());
        assert!(!manager.is_daily_completed());

        // The challenge's own expected command satisfies it
        let expected = match &daily.challenge_type {
            ChallengeType::DailyCommand { command, .. } => command.clone(),
            other => panic!("daily challenge should be DailyCommand, got {:?}", other),
        };
        let completed = manager.check_command(&expected, &mut stats).unwrap();
        assert_eq!(completed.id, daily.id);
        assert!(manager.is_daily_completed());

        // UserStats is kept in sync (single source of truth for achievements)
        assert!(stats.completed_challenges.contains(&daily.id));
        assert!(stats.completed_challenges.contains("first_challenge"));
        assert!(stats.achievements.is_unlocked("challenge_accepted"));

        // Already-completed challenges don't complete again
        assert!(manager.check_command(&expected, &mut stats).is_none());
    }

    #[test]
    fn test_check_command_weekly_scenario_steps() {
        let mut manager = ChallengeManager::new();
        let mut stats = UserStats::new();

        // Pin a known weekly scenario (Debug the Broken Server)
        let weekly = all_weekly_challenges()[0].clone();
        assert_eq!(weekly.id, "weekly_debug_server");
        manager.weekly_challenge = Some(weekly.clone());

        // Step 1: check if the server process is running
        assert!(manager.check_command("ps aux | grep 8080", &mut stats).is_none());
        // Step 2: check the log files
        assert!(manager.check_command("tail -f /var/log/server.log", &mut stats).is_none());
        // A command that doesn't match the current step doesn't advance
        assert!(manager.check_command("echo not a match", &mut stats).is_none());
        // Step 3 (final): verify the port
        let completed = manager.check_command("lsof -i :8080", &mut stats).unwrap();
        assert_eq!(completed.id, weekly.id);
        assert!(manager.is_weekly_completed());
        assert!(stats.completed_challenges.contains(&weekly.id));
    }
}