use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
const WIZARD_STEP_COUNT: usize = 6;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum WizardStep {
#[default]
Welcome,
Providers,
Models,
McpServers,
EditorSync,
Verification,
Complete,
}
impl WizardStep {
pub fn number(&self) -> usize {
match self {
Self::Welcome => 0,
Self::Providers => 1,
Self::Models => 2,
Self::McpServers => 3,
Self::EditorSync => 4,
Self::Verification => 5,
Self::Complete => 6,
}
}
pub fn from_number(n: usize) -> Option<Self> {
match n {
0 => Some(Self::Welcome),
1 => Some(Self::Providers),
2 => Some(Self::Models),
3 => Some(Self::McpServers),
4 => Some(Self::EditorSync),
5 => Some(Self::Verification),
6 => Some(Self::Complete),
_ => None,
}
}
pub fn title(&self) -> &'static str {
match self {
Self::Welcome => "Welcome",
Self::Providers => "Cloud Providers",
Self::Models => "Local Models",
Self::McpServers => "MCP Servers",
Self::EditorSync => "Editor Sync",
Self::Verification => "Verification",
Self::Complete => "Complete",
}
}
pub fn description(&self) -> &'static str {
match self {
Self::Welcome => "Introduction to Nika and setup overview",
Self::Providers => "Configure API keys for Claude, OpenAI, Mistral, etc.",
Self::Models => "Download local models for offline inference",
Self::McpServers => "Configure MCP servers (Neo4j, GitHub, etc.)",
Self::EditorSync => "Sync configuration to your editors",
Self::Verification => "Verify all components are working",
Self::Complete => "Setup completed successfully",
}
}
pub fn next(&self) -> Option<Self> {
match self {
Self::Welcome => Some(Self::Providers),
Self::Providers => Some(Self::Models),
Self::Models => Some(Self::McpServers),
Self::McpServers => Some(Self::EditorSync),
Self::EditorSync => Some(Self::Verification),
Self::Verification => Some(Self::Complete),
Self::Complete => None,
}
}
pub fn prev(&self) -> Option<Self> {
match self {
Self::Welcome => None,
Self::Providers => Some(Self::Welcome),
Self::Models => Some(Self::Providers),
Self::McpServers => Some(Self::Models),
Self::EditorSync => Some(Self::McpServers),
Self::Verification => Some(Self::EditorSync),
Self::Complete => Some(Self::Verification),
}
}
pub fn is_first(&self) -> bool {
matches!(self, Self::Welcome)
}
pub fn is_last(&self) -> bool {
matches!(self, Self::Verification)
}
pub fn is_complete(&self) -> bool {
matches!(self, Self::Complete)
}
pub fn all_active() -> &'static [WizardStep] {
&[
Self::Welcome,
Self::Providers,
Self::Models,
Self::McpServers,
Self::EditorSync,
Self::Verification,
]
}
}
impl std::fmt::Display for WizardStep {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.title())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WizardConfig {
pub completed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub completed_at: Option<DateTime<Utc>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub skipped_steps: Vec<WizardStep>,
}
impl WizardConfig {
pub fn completed() -> Self {
Self {
completed: true,
completed_at: Some(Utc::now()),
skipped_steps: Vec::new(),
}
}
pub fn completed_with_skipped(skipped: Vec<WizardStep>) -> Self {
Self {
completed: true,
completed_at: Some(Utc::now()),
skipped_steps: skipped,
}
}
pub fn was_skipped(&self, step: WizardStep) -> bool {
self.skipped_steps.contains(&step)
}
}
#[derive(Debug, Clone)]
pub struct WizardState {
pub current_step: WizardStep,
pub completed_steps: HashSet<WizardStep>,
pub provider_keys: HashMap<String, bool>,
pub selected_model: Option<String>,
pub enabled_editors: Vec<String>,
pub mcp_servers: Vec<String>,
pub errors: Vec<String>,
skipped_steps: HashSet<WizardStep>,
}
impl Default for WizardState {
fn default() -> Self {
Self::new()
}
}
impl WizardState {
pub fn new() -> Self {
Self {
current_step: WizardStep::Welcome,
completed_steps: HashSet::new(),
provider_keys: HashMap::new(),
selected_model: None,
enabled_editors: Vec::new(),
mcp_servers: Vec::new(),
errors: Vec::new(),
skipped_steps: HashSet::new(),
}
}
pub fn advance(&mut self) -> bool {
if let Some(next) = self.current_step.next() {
self.current_step = next;
true
} else {
false
}
}
pub fn go_back(&mut self) -> bool {
if let Some(prev) = self.current_step.prev() {
self.current_step = prev;
true
} else {
false
}
}
pub fn is_step_complete(&self, step: WizardStep) -> bool {
self.completed_steps.contains(&step)
}
pub fn mark_complete(&mut self, step: WizardStep) {
self.completed_steps.insert(step);
}
pub fn mark_incomplete(&mut self, step: WizardStep) {
self.completed_steps.remove(&step);
}
pub fn skip_current(&mut self) -> bool {
self.skipped_steps.insert(self.current_step);
self.advance()
}
pub fn is_step_skipped(&self, step: WizardStep) -> bool {
self.skipped_steps.contains(&step)
}
pub fn can_advance(&self) -> bool {
match self.current_step {
WizardStep::Welcome => true,
WizardStep::Providers => {
self.provider_keys.values().any(|&has_key| has_key)
}
WizardStep::Models => {
true
}
WizardStep::McpServers => {
true
}
WizardStep::EditorSync => {
true
}
WizardStep::Verification => {
self.is_step_complete(WizardStep::Welcome)
&& (self.is_step_complete(WizardStep::Providers)
|| self.is_step_skipped(WizardStep::Providers))
}
WizardStep::Complete => false,
}
}
pub fn progress_percentage(&self) -> u8 {
if self.current_step.is_complete() {
return 100;
}
let completed = self.completed_steps.len();
let total = WIZARD_STEP_COUNT;
((completed as f64 / total as f64) * 100.0).round() as u8
}
pub fn completed_count(&self) -> usize {
self.completed_steps.len()
}
pub fn total_steps(&self) -> usize {
WIZARD_STEP_COUNT
}
pub fn is_wizard_complete(&self) -> bool {
self.current_step.is_complete()
}
pub fn set_provider_key(&mut self, provider: impl Into<String>, has_key: bool) {
self.provider_keys.insert(provider.into(), has_key);
}
pub fn has_provider_key(&self, provider: &str) -> bool {
self.provider_keys.get(provider).copied().unwrap_or(false)
}
pub fn configured_provider_count(&self) -> usize {
self.provider_keys.values().filter(|&&v| v).count()
}
pub fn set_model(&mut self, model: impl Into<String>) {
self.selected_model = Some(model.into());
}
pub fn clear_model(&mut self) {
self.selected_model = None;
}
pub fn add_mcp_server(&mut self, server: impl Into<String>) {
let server = server.into();
if !self.mcp_servers.contains(&server) {
self.mcp_servers.push(server);
}
}
pub fn remove_mcp_server(&mut self, server: &str) {
self.mcp_servers.retain(|s| s != server);
}
pub fn enable_editor(&mut self, editor: &str) {
if !self.enabled_editors.iter().any(|e| e == editor) {
self.enabled_editors.push(editor.to_string());
}
}
pub fn disable_editor(&mut self, editor: &str) {
self.enabled_editors.retain(|e| e != editor);
}
pub fn is_editor_enabled(&self, editor: &str) -> bool {
self.enabled_editors.iter().any(|e| e == editor)
}
pub fn add_error(&mut self, error: impl Into<String>) {
self.errors.push(error.into());
}
pub fn clear_errors(&mut self) {
self.errors.clear();
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn to_config(&self) -> WizardConfig {
if self.is_wizard_complete() {
WizardConfig::completed_with_skipped(self.skipped_steps.iter().copied().collect())
} else {
WizardConfig::default()
}
}
pub fn reset(&mut self) {
*self = Self::new();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_step_default_is_welcome() {
assert_eq!(WizardStep::default(), WizardStep::Welcome);
}
#[test]
fn test_step_numbers() {
assert_eq!(WizardStep::Welcome.number(), 0);
assert_eq!(WizardStep::Providers.number(), 1);
assert_eq!(WizardStep::Models.number(), 2);
assert_eq!(WizardStep::McpServers.number(), 3);
assert_eq!(WizardStep::EditorSync.number(), 4);
assert_eq!(WizardStep::Verification.number(), 5);
assert_eq!(WizardStep::Complete.number(), 6);
}
#[test]
fn test_step_from_number() {
assert_eq!(WizardStep::from_number(0), Some(WizardStep::Welcome));
assert_eq!(WizardStep::from_number(1), Some(WizardStep::Providers));
assert_eq!(WizardStep::from_number(6), Some(WizardStep::Complete));
assert_eq!(WizardStep::from_number(7), None);
}
#[test]
fn test_step_next_chain() {
let mut step = WizardStep::Welcome;
step = step.next().unwrap();
assert_eq!(step, WizardStep::Providers);
step = step.next().unwrap();
assert_eq!(step, WizardStep::Models);
step = step.next().unwrap();
assert_eq!(step, WizardStep::McpServers);
step = step.next().unwrap();
assert_eq!(step, WizardStep::EditorSync);
step = step.next().unwrap();
assert_eq!(step, WizardStep::Verification);
step = step.next().unwrap();
assert_eq!(step, WizardStep::Complete);
assert!(step.next().is_none());
}
#[test]
fn test_step_prev_chain() {
let mut step = WizardStep::Complete;
step = step.prev().unwrap();
assert_eq!(step, WizardStep::Verification);
step = step.prev().unwrap();
assert_eq!(step, WizardStep::EditorSync);
step = step.prev().unwrap();
assert_eq!(step, WizardStep::McpServers);
step = step.prev().unwrap();
assert_eq!(step, WizardStep::Models);
step = step.prev().unwrap();
assert_eq!(step, WizardStep::Providers);
step = step.prev().unwrap();
assert_eq!(step, WizardStep::Welcome);
assert!(step.prev().is_none());
}
#[test]
fn test_step_is_first_last_complete() {
assert!(WizardStep::Welcome.is_first());
assert!(!WizardStep::Providers.is_first());
assert!(WizardStep::Verification.is_last());
assert!(!WizardStep::EditorSync.is_last());
assert!(WizardStep::Complete.is_complete());
assert!(!WizardStep::Verification.is_complete());
}
#[test]
fn test_step_all_active() {
let active = WizardStep::all_active();
assert_eq!(active.len(), 6);
assert!(!active.contains(&WizardStep::Complete));
}
#[test]
fn test_step_titles() {
assert_eq!(WizardStep::Welcome.title(), "Welcome");
assert_eq!(WizardStep::Providers.title(), "Cloud Providers");
assert_eq!(WizardStep::Models.title(), "Local Models");
assert_eq!(WizardStep::McpServers.title(), "MCP Servers");
assert_eq!(WizardStep::EditorSync.title(), "Editor Sync");
assert_eq!(WizardStep::Verification.title(), "Verification");
assert_eq!(WizardStep::Complete.title(), "Complete");
}
#[test]
fn test_step_display() {
assert_eq!(format!("{}", WizardStep::Welcome), "Welcome");
assert_eq!(format!("{}", WizardStep::Complete), "Complete");
}
#[test]
fn test_config_default() {
let config = WizardConfig::default();
assert!(!config.completed);
assert!(config.completed_at.is_none());
assert!(config.skipped_steps.is_empty());
}
#[test]
fn test_config_completed() {
let config = WizardConfig::completed();
assert!(config.completed);
assert!(config.completed_at.is_some());
assert!(config.skipped_steps.is_empty());
}
#[test]
fn test_config_completed_with_skipped() {
let config = WizardConfig::completed_with_skipped(vec![WizardStep::Models]);
assert!(config.completed);
assert!(config.was_skipped(WizardStep::Models));
assert!(!config.was_skipped(WizardStep::Providers));
}
#[test]
fn test_config_serialization() {
let config = WizardConfig::completed_with_skipped(vec![WizardStep::Models]);
let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("\"completed\":true"));
assert!(json.contains("\"models\""));
}
#[test]
fn test_state_new() {
let state = WizardState::new();
assert_eq!(state.current_step, WizardStep::Welcome);
assert!(state.completed_steps.is_empty());
assert!(state.provider_keys.is_empty());
assert!(state.selected_model.is_none());
assert!(state.enabled_editors.is_empty());
assert!(state.mcp_servers.is_empty());
assert!(state.errors.is_empty());
}
#[test]
fn test_state_default() {
let state = WizardState::default();
assert_eq!(state.current_step, WizardStep::Welcome);
}
#[test]
fn test_state_advance() {
let mut state = WizardState::new();
assert!(state.advance());
assert_eq!(state.current_step, WizardStep::Providers);
assert!(state.advance());
assert_eq!(state.current_step, WizardStep::Models);
state.advance();
state.advance();
state.advance();
state.advance();
assert_eq!(state.current_step, WizardStep::Complete);
assert!(!state.advance());
assert_eq!(state.current_step, WizardStep::Complete);
}
#[test]
fn test_state_go_back() {
let mut state = WizardState::new();
state.current_step = WizardStep::Models;
assert!(state.go_back());
assert_eq!(state.current_step, WizardStep::Providers);
assert!(state.go_back());
assert_eq!(state.current_step, WizardStep::Welcome);
assert!(!state.go_back());
assert_eq!(state.current_step, WizardStep::Welcome);
}
#[test]
fn test_state_mark_complete() {
let mut state = WizardState::new();
assert!(!state.is_step_complete(WizardStep::Welcome));
state.mark_complete(WizardStep::Welcome);
assert!(state.is_step_complete(WizardStep::Welcome));
assert!(!state.is_step_complete(WizardStep::Providers));
state.mark_incomplete(WizardStep::Welcome);
assert!(!state.is_step_complete(WizardStep::Welcome));
}
#[test]
fn test_state_skip_current() {
let mut state = WizardState::new();
state.current_step = WizardStep::Models;
assert!(state.skip_current());
assert_eq!(state.current_step, WizardStep::McpServers);
assert!(state.is_step_skipped(WizardStep::Models));
assert!(!state.is_step_complete(WizardStep::Models));
}
#[test]
fn test_state_can_advance_welcome() {
let state = WizardState::new();
assert!(state.can_advance());
}
#[test]
fn test_state_can_advance_providers() {
let mut state = WizardState::new();
state.current_step = WizardStep::Providers;
assert!(!state.can_advance());
state.set_provider_key("anthropic", true);
assert!(state.can_advance());
}
#[test]
fn test_state_can_advance_optional_steps() {
let mut state = WizardState::new();
state.current_step = WizardStep::Models;
assert!(state.can_advance());
state.current_step = WizardStep::McpServers;
assert!(state.can_advance());
state.current_step = WizardStep::EditorSync;
assert!(state.can_advance());
}
#[test]
fn test_state_can_advance_verification() {
let mut state = WizardState::new();
state.current_step = WizardStep::Verification;
assert!(!state.can_advance());
state.mark_complete(WizardStep::Welcome);
assert!(!state.can_advance());
state.skipped_steps.insert(WizardStep::Providers);
assert!(state.can_advance());
}
#[test]
fn test_state_can_advance_complete() {
let mut state = WizardState::new();
state.current_step = WizardStep::Complete;
assert!(!state.can_advance());
}
#[test]
fn test_state_progress_percentage() {
let mut state = WizardState::new();
assert_eq!(state.progress_percentage(), 0);
state.mark_complete(WizardStep::Welcome);
assert_eq!(state.progress_percentage(), 17);
state.mark_complete(WizardStep::Providers);
state.mark_complete(WizardStep::Models);
assert_eq!(state.progress_percentage(), 50);
state.mark_complete(WizardStep::McpServers);
state.mark_complete(WizardStep::EditorSync);
state.mark_complete(WizardStep::Verification);
assert_eq!(state.progress_percentage(), 100);
}
#[test]
fn test_state_progress_at_complete() {
let mut state = WizardState::new();
state.current_step = WizardStep::Complete;
assert_eq!(state.progress_percentage(), 100);
}
#[test]
fn test_state_completed_count() {
let mut state = WizardState::new();
assert_eq!(state.completed_count(), 0);
state.mark_complete(WizardStep::Welcome);
state.mark_complete(WizardStep::Providers);
assert_eq!(state.completed_count(), 2);
}
#[test]
fn test_state_total_steps() {
let state = WizardState::new();
assert_eq!(state.total_steps(), 6);
}
#[test]
fn test_state_is_wizard_complete() {
let mut state = WizardState::new();
assert!(!state.is_wizard_complete());
state.current_step = WizardStep::Complete;
assert!(state.is_wizard_complete());
}
#[test]
fn test_state_provider_keys() {
let mut state = WizardState::new();
assert!(!state.has_provider_key("anthropic"));
assert_eq!(state.configured_provider_count(), 0);
state.set_provider_key("anthropic", true);
assert!(state.has_provider_key("anthropic"));
assert_eq!(state.configured_provider_count(), 1);
state.set_provider_key("openai", true);
state.set_provider_key("mistral", false);
assert_eq!(state.configured_provider_count(), 2);
state.set_provider_key("anthropic", false);
assert!(!state.has_provider_key("anthropic"));
assert_eq!(state.configured_provider_count(), 1);
}
#[test]
fn test_state_model() {
let mut state = WizardState::new();
assert!(state.selected_model.is_none());
state.set_model("llama3.2:1b");
assert_eq!(state.selected_model, Some("llama3.2:1b".to_string()));
state.clear_model();
assert!(state.selected_model.is_none());
}
#[test]
fn test_state_mcp_servers() {
let mut state = WizardState::new();
assert!(state.mcp_servers.is_empty());
state.add_mcp_server("neo4j");
state.add_mcp_server("github");
assert_eq!(state.mcp_servers.len(), 2);
state.add_mcp_server("neo4j");
assert_eq!(state.mcp_servers.len(), 2);
state.remove_mcp_server("neo4j");
assert_eq!(state.mcp_servers, vec!["github"]);
}
#[test]
fn test_state_editors() {
let mut state = WizardState::new();
assert!(state.enabled_editors.is_empty());
assert!(!state.is_editor_enabled("claude-code"));
state.enable_editor("claude-code");
assert!(state.is_editor_enabled("claude-code"));
assert!(!state.is_editor_enabled("cursor"));
state.enable_editor("claude-code");
assert_eq!(state.enabled_editors.len(), 1);
state.enable_editor("cursor");
assert_eq!(state.enabled_editors.len(), 2);
state.disable_editor("claude-code");
assert!(!state.is_editor_enabled("claude-code"));
assert_eq!(state.enabled_editors.len(), 1);
}
#[test]
fn test_state_errors() {
let mut state = WizardState::new();
assert!(!state.has_errors());
state.add_error("Connection failed");
state.add_error("Invalid key format");
assert!(state.has_errors());
assert_eq!(state.errors.len(), 2);
state.clear_errors();
assert!(!state.has_errors());
}
#[test]
fn test_state_to_config_incomplete() {
let state = WizardState::new();
let config = state.to_config();
assert!(!config.completed);
}
#[test]
fn test_state_to_config_complete() {
let mut state = WizardState::new();
state.current_step = WizardStep::Complete;
state.skipped_steps.insert(WizardStep::Models);
let config = state.to_config();
assert!(config.completed);
assert!(config.was_skipped(WizardStep::Models));
}
#[test]
fn test_state_reset() {
let mut state = WizardState::new();
state.current_step = WizardStep::Models;
state.mark_complete(WizardStep::Welcome);
state.set_provider_key("anthropic", true);
state.set_model("llama3.2");
state.add_error("test error");
state.reset();
assert_eq!(state.current_step, WizardStep::Welcome);
assert!(state.completed_steps.is_empty());
assert!(state.provider_keys.is_empty());
assert!(state.selected_model.is_none());
assert!(state.errors.is_empty());
}
}