Skip to main content

mockforge_ui/handlers/
workspaces.rs

1//! Workspace management API handlers
2//!
3//! This module provides REST API endpoints for managing multi-tenant workspaces.
4
5use axum::{
6    extract::{Path, State},
7    http::StatusCode,
8    response::{IntoResponse, Json, Response},
9};
10use mockforge_core::{
11    workspace::{EnvironmentColor, MockEnvironmentName, SyncDirection, SyncDirectoryStructure},
12    MultiTenantWorkspaceRegistry, Workspace, WorkspaceStats,
13};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use std::{path::PathBuf, sync::Arc};
17
18/// Workspace management state
19#[derive(Debug, Clone)]
20pub struct WorkspaceState {
21    /// Multi-tenant workspace registry
22    pub registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>,
23}
24
25impl WorkspaceState {
26    /// Create a new workspace state
27    pub fn new(registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>) -> Self {
28        Self { registry }
29    }
30}
31
32/// API response wrapper
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ApiResponse<T> {
35    pub success: bool,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub data: Option<T>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub error: Option<String>,
40}
41
42impl<T: Serialize> ApiResponse<T> {
43    pub fn success(data: T) -> Self {
44        Self {
45            success: true,
46            data: Some(data),
47            error: None,
48        }
49    }
50
51    pub fn error(message: String) -> Self {
52        Self {
53            success: false,
54            data: None,
55            error: Some(message),
56        }
57    }
58}
59
60/// Workspace list item for API responses
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct WorkspaceListItem {
63    pub id: String,
64    pub name: String,
65    pub description: Option<String>,
66    pub enabled: bool,
67    pub stats: WorkspaceStats,
68    pub created_at: String,
69    pub updated_at: String,
70}
71
72/// Workspace creation request
73#[derive(Debug, Clone, Deserialize)]
74pub struct CreateWorkspaceRequest {
75    pub id: String,
76    pub name: String,
77    pub description: Option<String>,
78}
79
80/// Workspace update request
81#[derive(Debug, Clone, Deserialize)]
82pub struct UpdateWorkspaceRequest {
83    pub name: Option<String>,
84    pub description: Option<String>,
85    pub enabled: Option<bool>,
86}
87
88/// List all workspaces
89pub async fn list_workspaces(
90    State(state): State<WorkspaceState>,
91) -> Result<Json<ApiResponse<Vec<WorkspaceListItem>>>, Response> {
92    let registry = state.registry.read().await;
93
94    match registry.list_workspaces() {
95        Ok(workspaces) => {
96            let items: Vec<WorkspaceListItem> = workspaces
97                .into_iter()
98                .map(|(id, tenant_ws)| WorkspaceListItem {
99                    id,
100                    name: tenant_ws.workspace.name.clone(),
101                    description: tenant_ws.workspace.description.clone(),
102                    enabled: tenant_ws.enabled,
103                    stats: tenant_ws.stats.clone(),
104                    created_at: tenant_ws.workspace.created_at.to_rfc3339(),
105                    updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
106                })
107                .collect();
108
109            Ok(Json(ApiResponse::success(items)))
110        }
111        Err(e) => {
112            tracing::error!("Failed to list workspaces: {}", e);
113            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
114                .into_response())
115        }
116    }
117}
118
119/// Get a specific workspace by ID
120pub async fn get_workspace(
121    State(state): State<WorkspaceState>,
122    Path(workspace_id): Path<String>,
123) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
124    let registry = state.registry.read().await;
125
126    match registry.get_workspace(&workspace_id) {
127        Ok(tenant_ws) => {
128            let item = WorkspaceListItem {
129                id: workspace_id.clone(),
130                name: tenant_ws.workspace.name.clone(),
131                description: tenant_ws.workspace.description.clone(),
132                enabled: tenant_ws.enabled,
133                stats: tenant_ws.stats.clone(),
134                created_at: tenant_ws.workspace.created_at.to_rfc3339(),
135                updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
136            };
137
138            Ok(Json(ApiResponse::success(item)))
139        }
140        Err(e) => {
141            tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
142            Err((
143                StatusCode::NOT_FOUND,
144                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
145            )
146                .into_response())
147        }
148    }
149}
150
151/// Create a new workspace
152pub async fn create_workspace(
153    State(state): State<WorkspaceState>,
154    Json(request): Json<CreateWorkspaceRequest>,
155) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
156    let mut registry = state.registry.write().await;
157
158    // Check if workspace already exists
159    if registry.workspace_exists(&request.id) {
160        return Err((
161            StatusCode::CONFLICT,
162            Json(json!({"error": format!("Workspace '{}' already exists", request.id)})),
163        )
164            .into_response());
165    }
166
167    // Create new workspace
168    let mut workspace = Workspace::new(request.name.clone());
169    workspace.description = request.description.clone();
170
171    match registry.register_workspace(request.id.clone(), workspace) {
172        Ok(_) => {
173            // Get the created workspace
174            match registry.get_workspace(&request.id) {
175                Ok(tenant_ws) => {
176                    let item = WorkspaceListItem {
177                        id: request.id.clone(),
178                        name: tenant_ws.workspace.name.clone(),
179                        description: tenant_ws.workspace.description.clone(),
180                        enabled: tenant_ws.enabled,
181                        stats: tenant_ws.stats.clone(),
182                        created_at: tenant_ws.workspace.created_at.to_rfc3339(),
183                        updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
184                    };
185
186                    tracing::info!("Created workspace: {}", request.id);
187                    Ok(Json(ApiResponse::success(item)))
188                }
189                Err(e) => {
190                    tracing::error!("Failed to retrieve created workspace: {}", e);
191                    Err((
192                        StatusCode::INTERNAL_SERVER_ERROR,
193                        Json(json!({"error": "Workspace created but failed to retrieve"})),
194                    )
195                        .into_response())
196                }
197            }
198        }
199        Err(e) => {
200            tracing::error!("Failed to create workspace: {}", e);
201            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
202                .into_response())
203        }
204    }
205}
206
207/// Update an existing workspace
208pub async fn update_workspace(
209    State(state): State<WorkspaceState>,
210    Path(workspace_id): Path<String>,
211    Json(request): Json<UpdateWorkspaceRequest>,
212) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
213    let mut registry = state.registry.write().await;
214
215    // Get existing workspace
216    let mut tenant_ws = match registry.get_workspace(&workspace_id) {
217        Ok(ws) => ws,
218        Err(_e) => {
219            return Err((
220                StatusCode::NOT_FOUND,
221                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
222            )
223                .into_response());
224        }
225    };
226
227    // Update workspace fields
228    if let Some(name) = request.name {
229        tenant_ws.workspace.name = name;
230    }
231
232    if let Some(description) = request.description {
233        tenant_ws.workspace.description = Some(description);
234    }
235
236    tenant_ws.workspace.updated_at = chrono::Utc::now();
237
238    // Save updated workspace
239    match registry.update_workspace(&workspace_id, tenant_ws.workspace.clone()) {
240        Ok(_) => {
241            // Handle enabled/disabled separately
242            if let Some(enabled) = request.enabled {
243                if let Err(e) = registry.set_workspace_enabled(&workspace_id, enabled) {
244                    tracing::error!("Failed to set workspace enabled status: {}", e);
245                }
246            }
247
248            // Get updated workspace
249            match registry.get_workspace(&workspace_id) {
250                Ok(updated_ws) => {
251                    let item = WorkspaceListItem {
252                        id: workspace_id.clone(),
253                        name: updated_ws.workspace.name.clone(),
254                        description: updated_ws.workspace.description.clone(),
255                        enabled: updated_ws.enabled,
256                        stats: updated_ws.stats.clone(),
257                        created_at: updated_ws.workspace.created_at.to_rfc3339(),
258                        updated_at: updated_ws.workspace.updated_at.to_rfc3339(),
259                    };
260
261                    tracing::info!("Updated workspace: {}", workspace_id);
262                    Ok(Json(ApiResponse::success(item)))
263                }
264                Err(e) => {
265                    tracing::error!("Failed to retrieve updated workspace: {}", e);
266                    Err((
267                        StatusCode::INTERNAL_SERVER_ERROR,
268                        Json(json!({"error": "Workspace updated but failed to retrieve"})),
269                    )
270                        .into_response())
271                }
272            }
273        }
274        Err(e) => {
275            tracing::error!("Failed to update workspace: {}", e);
276            Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
277                .into_response())
278        }
279    }
280}
281
282/// Set active workspace (currently validates workspace existence and returns activation result)
283pub async fn set_active_workspace(
284    State(state): State<WorkspaceState>,
285    Path(workspace_id): Path<String>,
286) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
287    let registry = state.registry.read().await;
288
289    match registry.get_workspace(&workspace_id) {
290        Ok(_) => Ok(Json(ApiResponse::success(json!({
291            "workspace_id": workspace_id,
292            "active": true
293        })))),
294        Err(_) => Err((
295            StatusCode::NOT_FOUND,
296            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
297        )
298            .into_response()),
299    }
300}
301
302/// Delete a workspace
303pub async fn delete_workspace(
304    State(state): State<WorkspaceState>,
305    Path(workspace_id): Path<String>,
306) -> Result<Json<ApiResponse<String>>, Response> {
307    let mut registry = state.registry.write().await;
308
309    match registry.remove_workspace(&workspace_id) {
310        Ok(_) => {
311            tracing::info!("Deleted workspace: {}", workspace_id);
312            Ok(Json(ApiResponse::success(format!(
313                "Workspace '{}' deleted successfully",
314                workspace_id
315            ))))
316        }
317        Err(e) => {
318            tracing::error!("Failed to delete workspace {}: {}", workspace_id, e);
319            Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response())
320        }
321    }
322}
323
324/// Get workspace statistics
325pub async fn get_workspace_stats(
326    State(state): State<WorkspaceState>,
327    Path(workspace_id): Path<String>,
328) -> Result<Json<ApiResponse<WorkspaceStats>>, Response> {
329    let registry = state.registry.read().await;
330
331    match registry.get_workspace(&workspace_id) {
332        Ok(tenant_ws) => Ok(Json(ApiResponse::success(tenant_ws.stats.clone()))),
333        Err(e) => {
334            tracing::error!("Failed to get workspace stats for {}: {}", workspace_id, e);
335            Err((
336                StatusCode::NOT_FOUND,
337                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
338            )
339                .into_response())
340        }
341    }
342}
343
344/// Mock environment response
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct MockEnvironmentResponse {
347    pub name: String,
348    pub id: String,
349    pub workspace_id: String,
350    pub reality_config: Option<serde_json::Value>,
351    pub chaos_config: Option<serde_json::Value>,
352    pub drift_budget_config: Option<serde_json::Value>,
353}
354
355/// Mock environment manager response
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct MockEnvironmentManagerResponse {
358    pub workspace_id: String,
359    pub active_environment: Option<String>,
360    pub environments: Vec<MockEnvironmentResponse>,
361}
362
363/// List all mock environments for a workspace
364pub async fn list_mock_environments(
365    State(state): State<WorkspaceState>,
366    Path(workspace_id): Path<String>,
367) -> Result<Json<ApiResponse<MockEnvironmentManagerResponse>>, Response> {
368    let registry = state.registry.read().await;
369
370    match registry.get_workspace(&workspace_id) {
371        Ok(tenant_ws) => {
372            let mock_envs = tenant_ws.workspace.get_mock_environments();
373            let environments: Vec<MockEnvironmentResponse> = mock_envs
374                .list_environments()
375                .into_iter()
376                .map(|env| MockEnvironmentResponse {
377                    name: env.name.as_str().to_string(),
378                    id: env.id.clone(),
379                    workspace_id: env.workspace_id.clone(),
380                    reality_config: env
381                        .reality_config
382                        .as_ref()
383                        .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
384                    chaos_config: env
385                        .chaos_config
386                        .as_ref()
387                        .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
388                    drift_budget_config: env
389                        .drift_budget_config
390                        .as_ref()
391                        .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
392                })
393                .collect();
394
395            let response = MockEnvironmentManagerResponse {
396                workspace_id: workspace_id.clone(),
397                active_environment: mock_envs.active_environment.map(|e| e.as_str().to_string()),
398                environments,
399            };
400
401            Ok(Json(ApiResponse::success(response)))
402        }
403        Err(e) => {
404            tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
405            Err((
406                StatusCode::NOT_FOUND,
407                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
408            )
409                .into_response())
410        }
411    }
412}
413
414/// Get a specific mock environment
415pub async fn get_mock_environment(
416    State(state): State<WorkspaceState>,
417    Path((workspace_id, env_name)): Path<(String, String)>,
418) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
419    let registry = state.registry.read().await;
420
421    let env_name_enum = match env_name.to_lowercase().as_str() {
422        "dev" => MockEnvironmentName::Dev,
423        "test" => MockEnvironmentName::Test,
424        "prod" => MockEnvironmentName::Prod,
425        _ => {
426            return Err((
427                StatusCode::BAD_REQUEST,
428                Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
429            )
430                .into_response());
431        }
432    };
433
434    match registry.get_workspace(&workspace_id) {
435        Ok(tenant_ws) => {
436            match tenant_ws.workspace.get_mock_environment(env_name_enum) {
437                Some(env) => {
438                    let response = MockEnvironmentResponse {
439                        name: env.name.as_str().to_string(),
440                        id: env.id.clone(),
441                        workspace_id: env.workspace_id.clone(),
442                        reality_config: env.reality_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
443                        chaos_config: env.chaos_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
444                        drift_budget_config: env.drift_budget_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
445                    };
446                    Ok(Json(ApiResponse::success(response)))
447                }
448                None => Err((
449                    StatusCode::NOT_FOUND,
450                    Json(json!({"error": format!("Environment '{}' not found in workspace '{}'", env_name, workspace_id)})),
451                )
452                    .into_response()),
453            }
454        }
455        Err(e) => {
456            tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
457            Err((
458                StatusCode::NOT_FOUND,
459                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
460            )
461                .into_response())
462        }
463    }
464}
465
466/// Set active mock environment
467#[derive(Debug, Clone, Deserialize)]
468pub struct SetActiveEnvironmentRequest {
469    pub environment: String,
470}
471
472pub async fn set_active_mock_environment(
473    State(state): State<WorkspaceState>,
474    Path(workspace_id): Path<String>,
475    Json(request): Json<SetActiveEnvironmentRequest>,
476) -> Result<Json<ApiResponse<String>>, Response> {
477    let mut registry = state.registry.write().await;
478
479    let env_name = match request.environment.to_lowercase().as_str() {
480        "dev" => MockEnvironmentName::Dev,
481        "test" => MockEnvironmentName::Test,
482        "prod" => MockEnvironmentName::Prod,
483        _ => {
484            return Err((
485                StatusCode::BAD_REQUEST,
486                Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", request.environment)})),
487            )
488                .into_response());
489        }
490    };
491
492    match registry.get_workspace(&workspace_id) {
493        Ok(mut tenant_ws) => {
494            match tenant_ws.workspace.set_active_mock_environment(env_name) {
495                Ok(_) => {
496                    // Save the updated workspace
497                    if let Err(e) =
498                        registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
499                    {
500                        tracing::error!("Failed to save workspace: {}", e);
501                        return Err((
502                            StatusCode::INTERNAL_SERVER_ERROR,
503                            Json(json!({"error": "Failed to save workspace"})),
504                        )
505                            .into_response());
506                    }
507
508                    tracing::info!(
509                        "Set active environment to '{}' for workspace '{}'",
510                        request.environment,
511                        workspace_id
512                    );
513                    Ok(Json(ApiResponse::success(format!(
514                        "Active environment set to '{}'",
515                        request.environment
516                    ))))
517                }
518                Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
519                    .into_response()),
520            }
521        }
522        Err(e) => {
523            tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
524            Err((
525                StatusCode::NOT_FOUND,
526                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
527            )
528                .into_response())
529        }
530    }
531}
532
533/// Update mock environment configuration
534#[derive(Debug, Clone, Deserialize)]
535pub struct UpdateMockEnvironmentRequest {
536    pub reality_config: Option<serde_json::Value>,
537    pub chaos_config: Option<serde_json::Value>,
538    pub drift_budget_config: Option<serde_json::Value>,
539}
540
541pub async fn update_mock_environment(
542    State(state): State<WorkspaceState>,
543    Path((workspace_id, env_name)): Path<(String, String)>,
544    Json(request): Json<UpdateMockEnvironmentRequest>,
545) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
546    let mut registry = state.registry.write().await;
547
548    let env_name_enum = match env_name.to_lowercase().as_str() {
549        "dev" => MockEnvironmentName::Dev,
550        "test" => MockEnvironmentName::Test,
551        "prod" => MockEnvironmentName::Prod,
552        _ => {
553            return Err((
554                StatusCode::BAD_REQUEST,
555                Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
556            )
557                .into_response());
558        }
559    };
560
561    match registry.get_workspace(&workspace_id) {
562        Ok(mut tenant_ws) => {
563            // Parse the configs from JSON
564            let reality_config =
565                request.reality_config.and_then(|v| serde_json::from_value(v).ok());
566            let chaos_config = request.chaos_config.and_then(|v| serde_json::from_value(v).ok());
567            let drift_budget_config =
568                request.drift_budget_config.and_then(|v| serde_json::from_value(v).ok());
569
570            // Update the environment config
571            match tenant_ws.workspace.set_mock_environment_config(
572                env_name_enum,
573                reality_config,
574                chaos_config,
575                drift_budget_config,
576            ) {
577                Ok(_) => {
578                    // Save the updated workspace
579                    if let Err(e) =
580                        registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
581                    {
582                        tracing::error!("Failed to save workspace: {}", e);
583                        return Err((
584                            StatusCode::INTERNAL_SERVER_ERROR,
585                            Json(json!({"error": "Failed to save workspace"})),
586                        )
587                            .into_response());
588                    }
589
590                    // Get the updated environment
591                    match tenant_ws.workspace.get_mock_environment(env_name_enum) {
592                        Some(env) => {
593                            let response = MockEnvironmentResponse {
594                                name: env.name.as_str().to_string(),
595                                id: env.id.clone(),
596                                workspace_id: env.workspace_id.clone(),
597                                reality_config: env.reality_config.as_ref().map(|c| {
598                                    serde_json::to_value(c).unwrap_or(serde_json::json!({}))
599                                }),
600                                chaos_config: env.chaos_config.as_ref().map(|c| {
601                                    serde_json::to_value(c).unwrap_or(serde_json::json!({}))
602                                }),
603                                drift_budget_config: env.drift_budget_config.as_ref().map(|c| {
604                                    serde_json::to_value(c).unwrap_or(serde_json::json!({}))
605                                }),
606                            };
607                            tracing::info!(
608                                "Updated environment '{}' for workspace '{}'",
609                                env_name,
610                                workspace_id
611                            );
612                            Ok(Json(ApiResponse::success(response)))
613                        }
614                        None => Err((
615                            StatusCode::INTERNAL_SERVER_ERROR,
616                            Json(json!({"error": "Failed to retrieve updated environment"})),
617                        )
618                            .into_response()),
619                    }
620                }
621                Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
622                    .into_response()),
623            }
624        }
625        Err(e) => {
626            tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
627            Err((
628                StatusCode::NOT_FOUND,
629                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
630            )
631                .into_response())
632        }
633    }
634}
635
636#[derive(Debug, Clone, Deserialize)]
637pub struct CreateEnvironmentRequest {
638    pub name: String,
639    pub description: Option<String>,
640}
641
642#[derive(Debug, Clone, Deserialize)]
643pub struct UpdateEnvironmentRequest {
644    pub name: Option<String>,
645    pub description: Option<String>,
646    pub color: Option<EnvironmentColor>,
647}
648
649#[derive(Debug, Clone, Deserialize)]
650pub struct UpdateEnvironmentsOrderRequest {
651    pub environment_ids: Vec<String>,
652}
653
654#[derive(Debug, Clone, Deserialize)]
655pub struct UpdateWorkspacesOrderRequest {
656    pub workspace_ids: Vec<String>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct EnvironmentVariableResponse {
661    pub id: String,
662    pub key: String,
663    pub value: String,
664    pub encrypted: bool,
665    #[serde(rename = "createdAt")]
666    pub created_at: String,
667}
668
669#[derive(Debug, Clone, Deserialize)]
670pub struct SetVariableRequest {
671    pub key: String,
672    pub value: String,
673}
674
675#[derive(Debug, Clone, Deserialize)]
676pub struct AutocompleteRequest {
677    pub input: String,
678    pub cursor_position: usize,
679    pub context: Option<String>,
680}
681
682#[derive(Debug, Clone, Serialize)]
683pub struct AutocompleteSuggestion {
684    pub text: String,
685    pub display_text: Option<String>,
686    pub kind: Option<String>,
687    pub documentation: Option<String>,
688}
689
690#[derive(Debug, Clone, Serialize)]
691pub struct AutocompleteResponse {
692    pub suggestions: Vec<AutocompleteSuggestion>,
693    pub start_position: usize,
694    pub end_position: usize,
695}
696
697#[derive(Debug, Clone, Deserialize)]
698pub struct ConfigureSyncRequest {
699    pub target_directory: String,
700    pub sync_direction: SyncDirection,
701    pub realtime_monitoring: bool,
702    pub directory_structure: Option<SyncDirectoryStructure>,
703    pub filename_pattern: Option<String>,
704}
705
706#[derive(Debug, Clone, Deserialize)]
707pub struct ConfirmSyncChangesRequest {
708    pub workspace_id: String,
709    pub changes: Vec<serde_json::Value>,
710    pub apply_all: bool,
711}
712
713/// List all environments for a workspace.
714pub async fn list_environments(
715    State(state): State<WorkspaceState>,
716    Path(workspace_id): Path<String>,
717) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
718    let registry = state.registry.read().await;
719    let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
720        (
721            StatusCode::NOT_FOUND,
722            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
723        )
724            .into_response()
725    })?;
726
727    let workspace = &tenant_ws.workspace;
728    let global_env_id = workspace.config.global_environment.id.clone();
729    let active_env_id = workspace.get_active_environment().id.clone();
730    let mut environments = Vec::new();
731
732    for env in workspace.get_environments_ordered() {
733        environments.push(json!({
734            "id": env.id.clone(),
735            "name": env.name.clone(),
736            "description": env.description.clone(),
737            "variable_count": env.variables.len(),
738            "is_global": env.id == global_env_id,
739            "active": env.id == active_env_id,
740            "color": env.color.clone(),
741            "order": env.order,
742        }));
743    }
744
745    Ok(Json(ApiResponse::success(json!({
746        "environments": environments,
747        "total": environments.len(),
748    }))))
749}
750
751/// Create a workspace environment.
752pub async fn create_environment(
753    State(state): State<WorkspaceState>,
754    Path(workspace_id): Path<String>,
755    Json(request): Json<CreateEnvironmentRequest>,
756) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
757    let mut registry = state.registry.write().await;
758    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
759        (
760            StatusCode::NOT_FOUND,
761            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
762        )
763            .into_response()
764    })?;
765
766    let env_id = tenant_ws
767        .workspace
768        .create_environment(request.name, request.description)
769        .map_err(|e| {
770            (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
771        })?;
772
773    registry
774        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
775        .map_err(|e| {
776            (
777                StatusCode::INTERNAL_SERVER_ERROR,
778                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
779            )
780                .into_response()
781        })?;
782
783    Ok(Json(ApiResponse::success(json!({
784        "id": env_id,
785        "message": "Environment created"
786    }))))
787}
788
789/// Update a workspace environment.
790pub async fn update_environment(
791    State(state): State<WorkspaceState>,
792    Path((workspace_id, environment_id)): Path<(String, String)>,
793    Json(request): Json<UpdateEnvironmentRequest>,
794) -> Result<Json<ApiResponse<String>>, Response> {
795    let mut registry = state.registry.write().await;
796    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
797        (
798            StatusCode::NOT_FOUND,
799            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
800        )
801            .into_response()
802    })?;
803
804    if let Some(name) = &request.name {
805        let name_conflict = tenant_ws
806            .workspace
807            .get_environments()
808            .iter()
809            .any(|env| env.id != environment_id && env.name == *name);
810        if name_conflict {
811            return Err((
812                StatusCode::BAD_REQUEST,
813                Json(json!({"error": format!("Environment with name '{}' already exists", name)})),
814            )
815                .into_response());
816        }
817    }
818
819    let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
820        (
821            StatusCode::NOT_FOUND,
822            Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
823        )
824            .into_response()
825    })?;
826
827    if let Some(name) = request.name {
828        env.name = name;
829    }
830    if let Some(description) = request.description {
831        env.description = Some(description);
832    }
833    if let Some(color) = request.color {
834        env.color = Some(color);
835    }
836    env.updated_at = chrono::Utc::now();
837
838    registry
839        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
840        .map_err(|e| {
841            (
842                StatusCode::INTERNAL_SERVER_ERROR,
843                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
844            )
845                .into_response()
846        })?;
847
848    Ok(Json(ApiResponse::success("Environment updated".to_string())))
849}
850
851/// Delete a workspace environment.
852pub async fn delete_environment(
853    State(state): State<WorkspaceState>,
854    Path((workspace_id, environment_id)): Path<(String, String)>,
855) -> Result<Json<ApiResponse<String>>, Response> {
856    let mut registry = state.registry.write().await;
857    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
858        (
859            StatusCode::NOT_FOUND,
860            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
861        )
862            .into_response()
863    })?;
864
865    tenant_ws.workspace.delete_environment(&environment_id).map_err(|e| {
866        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
867    })?;
868
869    registry
870        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
871        .map_err(|e| {
872            (
873                StatusCode::INTERNAL_SERVER_ERROR,
874                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
875            )
876                .into_response()
877        })?;
878
879    Ok(Json(ApiResponse::success("Environment deleted".to_string())))
880}
881
882/// Set active environment for a workspace.
883pub async fn set_active_environment(
884    State(state): State<WorkspaceState>,
885    Path((workspace_id, environment_id)): Path<(String, String)>,
886) -> Result<Json<ApiResponse<String>>, Response> {
887    let mut registry = state.registry.write().await;
888    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
889        (
890            StatusCode::NOT_FOUND,
891            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
892        )
893            .into_response()
894    })?;
895
896    tenant_ws.workspace.set_active_environment(Some(environment_id)).map_err(|e| {
897        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
898    })?;
899
900    registry
901        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
902        .map_err(|e| {
903            (
904                StatusCode::INTERNAL_SERVER_ERROR,
905                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
906            )
907                .into_response()
908        })?;
909
910    Ok(Json(ApiResponse::success("Environment activated".to_string())))
911}
912
913/// Update environment display order.
914pub async fn update_environments_order(
915    State(state): State<WorkspaceState>,
916    Path(workspace_id): Path<String>,
917    Json(request): Json<UpdateEnvironmentsOrderRequest>,
918) -> Result<Json<ApiResponse<String>>, Response> {
919    let mut registry = state.registry.write().await;
920    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
921        (
922            StatusCode::NOT_FOUND,
923            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
924        )
925            .into_response()
926    })?;
927
928    tenant_ws
929        .workspace
930        .update_environments_order(request.environment_ids)
931        .map_err(|e| {
932            (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
933        })?;
934
935    registry
936        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
937        .map_err(|e| {
938            (
939                StatusCode::INTERNAL_SERVER_ERROR,
940                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
941            )
942                .into_response()
943        })?;
944
945    Ok(Json(ApiResponse::success("Environment order updated".to_string())))
946}
947
948/// Update workspace display order.
949pub async fn update_workspaces_order(
950    State(state): State<WorkspaceState>,
951    Json(request): Json<UpdateWorkspacesOrderRequest>,
952) -> Result<Json<ApiResponse<String>>, Response> {
953    let mut registry = state.registry.write().await;
954
955    for workspace_id in &request.workspace_ids {
956        if !registry.workspace_exists(workspace_id) {
957            return Err((
958                StatusCode::NOT_FOUND,
959                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
960            )
961                .into_response());
962        }
963    }
964
965    for (idx, workspace_id) in request.workspace_ids.iter().enumerate() {
966        let mut tenant_ws = registry.get_workspace(workspace_id).map_err(|_| {
967            (
968                StatusCode::NOT_FOUND,
969                Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
970            )
971                .into_response()
972        })?;
973        tenant_ws.workspace.order = idx as i32;
974        tenant_ws.workspace.updated_at = chrono::Utc::now();
975        registry
976            .update_workspace(workspace_id, tenant_ws.workspace.clone())
977            .map_err(|e| {
978                (
979                    StatusCode::INTERNAL_SERVER_ERROR,
980                    Json(json!({"error": format!("Failed to save workspace: {}", e)})),
981                )
982                    .into_response()
983            })?;
984    }
985
986    Ok(Json(ApiResponse::success("Workspace order updated".to_string())))
987}
988
989/// Get all environment variables in context for the selected environment.
990pub async fn get_environment_variables(
991    State(state): State<WorkspaceState>,
992    Path((workspace_id, environment_id)): Path<(String, String)>,
993) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
994    let registry = state.registry.write().await;
995    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
996        (
997            StatusCode::NOT_FOUND,
998            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
999        )
1000            .into_response()
1001    })?;
1002
1003    tenant_ws.workspace.set_active_environment(Some(environment_id)).map_err(|e| {
1004        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1005    })?;
1006
1007    let now = chrono::Utc::now().to_rfc3339();
1008    let mut variables = Vec::new();
1009    for (key, value) in tenant_ws.workspace.get_all_variables() {
1010        variables.push(EnvironmentVariableResponse {
1011            id: key.clone(),
1012            key,
1013            value,
1014            encrypted: false,
1015            created_at: now.clone(),
1016        });
1017    }
1018
1019    Ok(Json(ApiResponse::success(json!({
1020        "variables": variables
1021    }))))
1022}
1023
1024/// Set or update an environment variable.
1025pub async fn set_environment_variable(
1026    State(state): State<WorkspaceState>,
1027    Path((workspace_id, environment_id)): Path<(String, String)>,
1028    Json(request): Json<SetVariableRequest>,
1029) -> Result<Json<ApiResponse<String>>, Response> {
1030    let mut registry = state.registry.write().await;
1031    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1032        (
1033            StatusCode::NOT_FOUND,
1034            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1035        )
1036            .into_response()
1037    })?;
1038
1039    let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
1040        (
1041            StatusCode::NOT_FOUND,
1042            Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
1043        )
1044            .into_response()
1045    })?;
1046
1047    env.set_variable(request.key, request.value);
1048
1049    registry
1050        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1051        .map_err(|e| {
1052            (
1053                StatusCode::INTERNAL_SERVER_ERROR,
1054                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1055            )
1056                .into_response()
1057        })?;
1058
1059    Ok(Json(ApiResponse::success("Environment variable set".to_string())))
1060}
1061
1062/// Remove an environment variable.
1063pub async fn remove_environment_variable(
1064    State(state): State<WorkspaceState>,
1065    Path((workspace_id, environment_id, variable_name)): Path<(String, String, String)>,
1066) -> Result<Json<ApiResponse<String>>, Response> {
1067    let mut registry = state.registry.write().await;
1068    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1069        (
1070            StatusCode::NOT_FOUND,
1071            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1072        )
1073            .into_response()
1074    })?;
1075
1076    let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
1077        (
1078            StatusCode::NOT_FOUND,
1079            Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
1080        )
1081            .into_response()
1082    })?;
1083
1084    if !env.remove_variable(&variable_name) {
1085        return Err((
1086            StatusCode::NOT_FOUND,
1087            Json(json!({"error": format!("Variable '{}' not found", variable_name)})),
1088        )
1089            .into_response());
1090    }
1091
1092    registry
1093        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1094        .map_err(|e| {
1095            (
1096                StatusCode::INTERNAL_SERVER_ERROR,
1097                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1098            )
1099                .into_response()
1100        })?;
1101
1102    Ok(Json(ApiResponse::success("Environment variable removed".to_string())))
1103}
1104
1105/// Generate autocomplete suggestions based on workspace variables and common tokens.
1106pub async fn get_autocomplete_suggestions(
1107    State(state): State<WorkspaceState>,
1108    Path(workspace_id): Path<String>,
1109    Json(request): Json<AutocompleteRequest>,
1110) -> Result<Json<ApiResponse<AutocompleteResponse>>, Response> {
1111    let registry = state.registry.read().await;
1112    let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1113        (
1114            StatusCode::NOT_FOUND,
1115            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1116        )
1117            .into_response()
1118    })?;
1119
1120    let input = request.input;
1121    let cursor = request.cursor_position.min(input.len());
1122    let bytes = input.as_bytes();
1123    let mut start = cursor;
1124    while start > 0 {
1125        let ch = bytes[start - 1] as char;
1126        if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || ch == '-' {
1127            start -= 1;
1128        } else {
1129            break;
1130        }
1131    }
1132    let prefix = &input[start..cursor];
1133    let prefix_lower = prefix.to_lowercase();
1134
1135    let mut suggestions: Vec<AutocompleteSuggestion> = Vec::new();
1136    for (key, _) in tenant_ws.workspace.get_all_variables() {
1137        if prefix.is_empty() || key.to_lowercase().contains(&prefix_lower) {
1138            suggestions.push(AutocompleteSuggestion {
1139                text: key.clone(),
1140                display_text: Some(key),
1141                kind: Some("variable".to_string()),
1142                documentation: Some("Workspace environment variable".to_string()),
1143            });
1144        }
1145    }
1146
1147    let builtins = [
1148        ("now", "Current timestamp"),
1149        ("uuid", "Generate UUID"),
1150        ("rand.int", "Random integer"),
1151        ("rand.float", "Random float"),
1152        ("faker.name", "Random name"),
1153        ("faker.email", "Random email"),
1154    ];
1155    for (token, doc) in builtins {
1156        if prefix.is_empty() || token.contains(prefix) {
1157            suggestions.push(AutocompleteSuggestion {
1158                text: token.to_string(),
1159                display_text: Some(token.to_string()),
1160                kind: Some("builtin".to_string()),
1161                documentation: Some(doc.to_string()),
1162            });
1163        }
1164    }
1165
1166    suggestions.sort_by(|a, b| a.text.cmp(&b.text));
1167    suggestions.dedup_by(|a, b| a.text == b.text);
1168    suggestions.truncate(20);
1169
1170    Ok(Json(ApiResponse::success(AutocompleteResponse {
1171        suggestions,
1172        start_position: start,
1173        end_position: cursor,
1174    })))
1175}
1176
1177/// Get current directory sync status.
1178pub async fn get_sync_status(
1179    State(state): State<WorkspaceState>,
1180    Path(workspace_id): Path<String>,
1181) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
1182    let registry = state.registry.read().await;
1183    let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1184        (
1185            StatusCode::NOT_FOUND,
1186            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1187        )
1188            .into_response()
1189    })?;
1190
1191    let sync = tenant_ws.workspace.get_sync_config();
1192    Ok(Json(ApiResponse::success(json!({
1193        "workspace_id": workspace_id,
1194        "enabled": sync.enabled,
1195        "target_directory": sync.target_directory,
1196        "sync_direction": sync.sync_direction,
1197        "realtime_monitoring": sync.realtime_monitoring,
1198        "last_sync": sync.last_sync,
1199        "status": if sync.enabled { "ready" } else { "disabled" },
1200    }))))
1201}
1202
1203/// Configure directory sync.
1204pub async fn configure_sync(
1205    State(state): State<WorkspaceState>,
1206    Path(workspace_id): Path<String>,
1207    Json(request): Json<ConfigureSyncRequest>,
1208) -> Result<Json<ApiResponse<String>>, Response> {
1209    let mut registry = state.registry.write().await;
1210    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1211        (
1212            StatusCode::NOT_FOUND,
1213            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1214        )
1215            .into_response()
1216    })?;
1217
1218    let mut sync = tenant_ws.workspace.get_sync_config().clone();
1219    sync.enabled = true;
1220    sync.target_directory = Some(request.target_directory);
1221    sync.sync_direction = request.sync_direction;
1222    sync.realtime_monitoring = request.realtime_monitoring;
1223    if let Some(directory_structure) = request.directory_structure {
1224        sync.directory_structure = directory_structure;
1225    }
1226    if let Some(filename_pattern) = request.filename_pattern {
1227        sync.filename_pattern = filename_pattern;
1228    }
1229
1230    tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1231        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1232    })?;
1233
1234    registry
1235        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1236        .map_err(|e| {
1237            (
1238                StatusCode::INTERNAL_SERVER_ERROR,
1239                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1240            )
1241                .into_response()
1242        })?;
1243
1244    Ok(Json(ApiResponse::success("Sync configured".to_string())))
1245}
1246
1247/// Disable directory sync.
1248pub async fn disable_sync(
1249    State(state): State<WorkspaceState>,
1250    Path(workspace_id): Path<String>,
1251) -> Result<Json<ApiResponse<String>>, Response> {
1252    let mut registry = state.registry.write().await;
1253    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1254        (
1255            StatusCode::NOT_FOUND,
1256            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1257        )
1258            .into_response()
1259    })?;
1260
1261    tenant_ws.workspace.disable_sync().map_err(|e| {
1262        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1263    })?;
1264
1265    registry
1266        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1267        .map_err(|e| {
1268            (
1269                StatusCode::INTERNAL_SERVER_ERROR,
1270                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1271            )
1272                .into_response()
1273        })?;
1274
1275    Ok(Json(ApiResponse::success("Sync disabled".to_string())))
1276}
1277
1278/// Trigger a manual sync operation.
1279pub async fn trigger_sync(
1280    State(state): State<WorkspaceState>,
1281    Path(workspace_id): Path<String>,
1282) -> Result<Json<ApiResponse<String>>, Response> {
1283    let mut registry = state.registry.write().await;
1284    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1285        (
1286            StatusCode::NOT_FOUND,
1287            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1288        )
1289            .into_response()
1290    })?;
1291
1292    let mut sync = tenant_ws.workspace.get_sync_config().clone();
1293    if !sync.enabled {
1294        return Err((StatusCode::BAD_REQUEST, Json(json!({"error": "Sync is not enabled"})))
1295            .into_response());
1296    }
1297    sync.last_sync = Some(chrono::Utc::now());
1298    tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1299        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1300    })?;
1301
1302    registry
1303        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1304        .map_err(|e| {
1305            (
1306                StatusCode::INTERNAL_SERVER_ERROR,
1307                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1308            )
1309                .into_response()
1310        })?;
1311
1312    Ok(Json(ApiResponse::success("Sync triggered".to_string())))
1313}
1314
1315#[derive(Debug, Clone, Serialize)]
1316struct SyncChangeItem {
1317    change_type: String,
1318    path: String,
1319    description: String,
1320    requires_confirmation: bool,
1321}
1322
1323fn collect_sync_changes(
1324    target_directory: PathBuf,
1325    last_sync: Option<chrono::DateTime<chrono::Utc>>,
1326) -> Vec<SyncChangeItem> {
1327    const MAX_CHANGES: usize = 250;
1328
1329    if !target_directory.exists() {
1330        return vec![SyncChangeItem {
1331            change_type: "created".to_string(),
1332            path: target_directory.display().to_string(),
1333            description: "Sync target directory does not exist yet and will be created during sync"
1334                .to_string(),
1335            requires_confirmation: false,
1336        }];
1337    }
1338
1339    let mut changes = Vec::new();
1340    let mut stack = vec![target_directory.clone()];
1341
1342    while let Some(current_dir) = stack.pop() {
1343        let entries = match std::fs::read_dir(&current_dir) {
1344            Ok(entries) => entries,
1345            Err(_) => continue,
1346        };
1347
1348        for entry in entries.flatten() {
1349            if changes.len() >= MAX_CHANGES {
1350                break;
1351            }
1352
1353            let path = entry.path();
1354            let metadata = match entry.metadata() {
1355                Ok(metadata) => metadata,
1356                Err(_) => continue,
1357            };
1358
1359            if metadata.is_dir() {
1360                stack.push(path);
1361                continue;
1362            }
1363
1364            let modified_after_sync = match (metadata.modified(), last_sync) {
1365                (Ok(modified), Some(last_sync_ts)) => {
1366                    let modified_utc = chrono::DateTime::<chrono::Utc>::from(modified);
1367                    modified_utc > last_sync_ts
1368                }
1369                (Ok(_), None) => true,
1370                (Err(_), _) => false,
1371            };
1372
1373            if !modified_after_sync {
1374                continue;
1375            }
1376
1377            let rel_path = path
1378                .strip_prefix(&target_directory)
1379                .map(|p| p.display().to_string())
1380                .unwrap_or_else(|_| path.display().to_string());
1381
1382            changes.push(SyncChangeItem {
1383                change_type: "modified".to_string(),
1384                path: rel_path.clone(),
1385                description: format!("Detected filesystem change in '{}'", rel_path),
1386                requires_confirmation: true,
1387            });
1388        }
1389
1390        if changes.len() >= MAX_CHANGES {
1391            break;
1392        }
1393    }
1394
1395    changes
1396}
1397
1398/// Get pending sync changes by comparing sync directory files against last sync timestamp.
1399pub async fn get_sync_changes(
1400    State(state): State<WorkspaceState>,
1401    Path(workspace_id): Path<String>,
1402) -> Result<Json<ApiResponse<Vec<serde_json::Value>>>, Response> {
1403    let registry = state.registry.read().await;
1404    let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1405        (
1406            StatusCode::NOT_FOUND,
1407            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1408        )
1409            .into_response()
1410    })?;
1411
1412    let sync = tenant_ws.workspace.get_sync_config().clone();
1413    let changes: Vec<serde_json::Value> = if !sync.enabled {
1414        Vec::new()
1415    } else if let Some(target_directory) = sync.target_directory.clone() {
1416        let target_directory = PathBuf::from(target_directory);
1417        tokio::task::spawn_blocking(move || collect_sync_changes(target_directory, sync.last_sync))
1418            .await
1419            .map_err(|e| {
1420                (
1421                    StatusCode::INTERNAL_SERVER_ERROR,
1422                    Json(json!({"error": format!("Failed to inspect sync directory: {}", e)})),
1423                )
1424                    .into_response()
1425            })?
1426            .into_iter()
1427            .map(|change| serde_json::to_value(change).unwrap_or_default())
1428            .collect()
1429    } else {
1430        Vec::new()
1431    };
1432
1433    Ok(Json(ApiResponse::success(changes)))
1434}
1435
1436/// Confirm and apply pending sync changes.
1437pub async fn confirm_sync_changes(
1438    State(state): State<WorkspaceState>,
1439    Path(workspace_id): Path<String>,
1440    Json(request): Json<ConfirmSyncChangesRequest>,
1441) -> Result<Json<ApiResponse<String>>, Response> {
1442    let mut registry = state.registry.write().await;
1443    let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1444        (
1445            StatusCode::NOT_FOUND,
1446            Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1447        )
1448            .into_response()
1449    })?;
1450
1451    if request.workspace_id != workspace_id {
1452        return Err((
1453            StatusCode::BAD_REQUEST,
1454            Json(json!({"error": "workspace_id in body must match path"})),
1455        )
1456            .into_response());
1457    }
1458
1459    let mut sync = tenant_ws.workspace.get_sync_config().clone();
1460    sync.last_sync = Some(chrono::Utc::now());
1461    tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1462        (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1463    })?;
1464
1465    registry
1466        .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1467        .map_err(|e| {
1468            (
1469                StatusCode::INTERNAL_SERVER_ERROR,
1470                Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1471            )
1472                .into_response()
1473        })?;
1474
1475    Ok(Json(ApiResponse::success(format!(
1476        "Sync changes confirmed ({} changes, apply_all={})",
1477        request.changes.len(),
1478        request.apply_all
1479    ))))
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484    use super::*;
1485    use mockforge_core::MultiTenantConfig;
1486
1487    fn create_test_state() -> WorkspaceState {
1488        let config = MultiTenantConfig::default();
1489        let registry = MultiTenantWorkspaceRegistry::new(config);
1490        WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)))
1491    }
1492
1493    // ==================== WorkspaceState Tests ====================
1494
1495    #[test]
1496    fn test_workspace_state_creation() {
1497        let state = create_test_state();
1498        // State is created - this verifies the type is correct
1499        let _ = state;
1500    }
1501
1502    #[test]
1503    fn test_workspace_state_clone() {
1504        let state = create_test_state();
1505        let cloned = state.clone();
1506        // Both states reference the same registry
1507        let _ = cloned;
1508    }
1509
1510    #[test]
1511    fn test_workspace_state_debug() {
1512        let state = create_test_state();
1513        let debug = format!("{:?}", state);
1514        assert!(debug.contains("WorkspaceState"));
1515    }
1516
1517    // ==================== ApiResponse Tests ====================
1518
1519    #[test]
1520    fn test_api_response_success() {
1521        let response: ApiResponse<String> = ApiResponse::success("test data".to_string());
1522        assert!(response.success);
1523        assert!(response.data.is_some());
1524        assert!(response.error.is_none());
1525    }
1526
1527    #[test]
1528    fn test_api_response_error() {
1529        let response: ApiResponse<String> = ApiResponse::error("test error".to_string());
1530        assert!(!response.success);
1531        assert!(response.data.is_none());
1532        assert!(response.error.is_some());
1533    }
1534
1535    #[test]
1536    fn test_api_response_serialization() {
1537        let response = ApiResponse::success("data".to_string());
1538        let json = serde_json::to_string(&response).unwrap();
1539        assert!(json.contains("success"));
1540        assert!(json.contains("data"));
1541    }
1542
1543    #[test]
1544    fn test_api_response_error_serialization() {
1545        let response: ApiResponse<()> = ApiResponse::error("something went wrong".to_string());
1546        let json = serde_json::to_string(&response).unwrap();
1547        assert!(json.contains("error"));
1548        assert!(json.contains("something went wrong"));
1549    }
1550
1551    // ==================== CreateWorkspaceRequest Tests ====================
1552
1553    #[test]
1554    fn test_create_workspace_request_minimal() {
1555        let request = CreateWorkspaceRequest {
1556            id: "ws-123".to_string(),
1557            name: "My Workspace".to_string(),
1558            description: None,
1559        };
1560
1561        assert_eq!(request.id, "ws-123");
1562        assert_eq!(request.name, "My Workspace");
1563        assert!(request.description.is_none());
1564    }
1565
1566    #[test]
1567    fn test_create_workspace_request_full() {
1568        let request = CreateWorkspaceRequest {
1569            id: "ws-456".to_string(),
1570            name: "Full Workspace".to_string(),
1571            description: Some("A complete workspace".to_string()),
1572        };
1573
1574        assert!(request.description.is_some());
1575    }
1576
1577    #[test]
1578    fn test_create_workspace_request_deserialization() {
1579        let json = r#"{
1580            "id": "test-ws",
1581            "name": "Test",
1582            "description": "Test workspace"
1583        }"#;
1584
1585        let request: CreateWorkspaceRequest = serde_json::from_str(json).unwrap();
1586        assert_eq!(request.id, "test-ws");
1587        assert_eq!(request.name, "Test");
1588    }
1589
1590    // ==================== UpdateWorkspaceRequest Tests ====================
1591
1592    #[test]
1593    fn test_update_workspace_request_empty() {
1594        let request = UpdateWorkspaceRequest {
1595            name: None,
1596            description: None,
1597            enabled: None,
1598        };
1599
1600        assert!(request.name.is_none());
1601        assert!(request.description.is_none());
1602        assert!(request.enabled.is_none());
1603    }
1604
1605    #[test]
1606    fn test_update_workspace_request_partial() {
1607        let request = UpdateWorkspaceRequest {
1608            name: Some("New Name".to_string()),
1609            description: None,
1610            enabled: Some(false),
1611        };
1612
1613        assert!(request.name.is_some());
1614        assert!(request.enabled.is_some());
1615    }
1616
1617    #[test]
1618    fn test_update_workspace_request_deserialization() {
1619        let json = r#"{
1620            "name": "Updated",
1621            "enabled": true
1622        }"#;
1623
1624        let request: UpdateWorkspaceRequest = serde_json::from_str(json).unwrap();
1625        assert_eq!(request.name, Some("Updated".to_string()));
1626        assert_eq!(request.enabled, Some(true));
1627    }
1628
1629    // ==================== WorkspaceListItem Tests ====================
1630
1631    #[test]
1632    fn test_workspace_list_item_creation() {
1633        let item = WorkspaceListItem {
1634            id: "item-1".to_string(),
1635            name: "Test Item".to_string(),
1636            description: Some("Description".to_string()),
1637            enabled: true,
1638            stats: WorkspaceStats::default(),
1639            created_at: "2024-01-01T00:00:00Z".to_string(),
1640            updated_at: "2024-01-02T00:00:00Z".to_string(),
1641        };
1642
1643        assert_eq!(item.id, "item-1");
1644        assert!(item.enabled);
1645    }
1646
1647    #[test]
1648    fn test_workspace_list_item_serialization() {
1649        let item = WorkspaceListItem {
1650            id: "ser-test".to_string(),
1651            name: "Serialize Test".to_string(),
1652            description: None,
1653            enabled: false,
1654            stats: WorkspaceStats::default(),
1655            created_at: "2024-01-01T00:00:00Z".to_string(),
1656            updated_at: "2024-01-01T00:00:00Z".to_string(),
1657        };
1658
1659        let json = serde_json::to_string(&item).unwrap();
1660        assert!(json.contains("ser-test"));
1661        assert!(json.contains("Serialize Test"));
1662    }
1663
1664    #[test]
1665    fn test_workspace_list_item_clone() {
1666        let item = WorkspaceListItem {
1667            id: "clone-test".to_string(),
1668            name: "Clone Test".to_string(),
1669            description: None,
1670            enabled: true,
1671            stats: WorkspaceStats::default(),
1672            created_at: "2024-01-01T00:00:00Z".to_string(),
1673            updated_at: "2024-01-01T00:00:00Z".to_string(),
1674        };
1675
1676        let cloned = item.clone();
1677        assert_eq!(cloned.id, item.id);
1678        assert_eq!(cloned.enabled, item.enabled);
1679    }
1680
1681    // ==================== MockEnvironmentResponse Tests ====================
1682
1683    #[test]
1684    fn test_mock_environment_response_creation() {
1685        let response = MockEnvironmentResponse {
1686            name: "dev".to_string(),
1687            id: "env-123".to_string(),
1688            workspace_id: "ws-456".to_string(),
1689            reality_config: None,
1690            chaos_config: None,
1691            drift_budget_config: None,
1692        };
1693
1694        assert_eq!(response.name, "dev");
1695        assert_eq!(response.id, "env-123");
1696    }
1697
1698    #[test]
1699    fn test_mock_environment_response_with_configs() {
1700        let response = MockEnvironmentResponse {
1701            name: "test".to_string(),
1702            id: "env-test".to_string(),
1703            workspace_id: "ws-test".to_string(),
1704            reality_config: Some(serde_json::json!({"level": "high"})),
1705            chaos_config: Some(serde_json::json!({"enabled": true})),
1706            drift_budget_config: Some(serde_json::json!({"max_drift": 0.1})),
1707        };
1708
1709        assert!(response.reality_config.is_some());
1710        assert!(response.chaos_config.is_some());
1711        assert!(response.drift_budget_config.is_some());
1712    }
1713
1714    #[test]
1715    fn test_mock_environment_response_serialization() {
1716        let response = MockEnvironmentResponse {
1717            name: "prod".to_string(),
1718            id: "env-prod".to_string(),
1719            workspace_id: "ws-prod".to_string(),
1720            reality_config: None,
1721            chaos_config: None,
1722            drift_budget_config: None,
1723        };
1724
1725        let json = serde_json::to_string(&response).unwrap();
1726        assert!(json.contains("prod"));
1727        assert!(json.contains("env-prod"));
1728    }
1729
1730    // ==================== MockEnvironmentManagerResponse Tests ====================
1731
1732    #[test]
1733    fn test_mock_environment_manager_response_empty() {
1734        let response = MockEnvironmentManagerResponse {
1735            workspace_id: "ws-empty".to_string(),
1736            active_environment: None,
1737            environments: vec![],
1738        };
1739
1740        assert!(response.active_environment.is_none());
1741        assert!(response.environments.is_empty());
1742    }
1743
1744    #[test]
1745    fn test_mock_environment_manager_response_with_environments() {
1746        let response = MockEnvironmentManagerResponse {
1747            workspace_id: "ws-full".to_string(),
1748            active_environment: Some("dev".to_string()),
1749            environments: vec![
1750                MockEnvironmentResponse {
1751                    name: "dev".to_string(),
1752                    id: "env-dev".to_string(),
1753                    workspace_id: "ws-full".to_string(),
1754                    reality_config: None,
1755                    chaos_config: None,
1756                    drift_budget_config: None,
1757                },
1758                MockEnvironmentResponse {
1759                    name: "test".to_string(),
1760                    id: "env-test".to_string(),
1761                    workspace_id: "ws-full".to_string(),
1762                    reality_config: None,
1763                    chaos_config: None,
1764                    drift_budget_config: None,
1765                },
1766            ],
1767        };
1768
1769        assert_eq!(response.active_environment, Some("dev".to_string()));
1770        assert_eq!(response.environments.len(), 2);
1771    }
1772
1773    // ==================== SetActiveEnvironmentRequest Tests ====================
1774
1775    #[test]
1776    fn test_set_active_environment_request_creation() {
1777        let request = SetActiveEnvironmentRequest {
1778            environment: "prod".to_string(),
1779        };
1780
1781        assert_eq!(request.environment, "prod");
1782    }
1783
1784    #[test]
1785    fn test_set_active_environment_request_deserialization() {
1786        let json = r#"{"environment": "test"}"#;
1787        let request: SetActiveEnvironmentRequest = serde_json::from_str(json).unwrap();
1788        assert_eq!(request.environment, "test");
1789    }
1790
1791    // ==================== UpdateMockEnvironmentRequest Tests ====================
1792
1793    #[test]
1794    fn test_update_mock_environment_request_empty() {
1795        let request = UpdateMockEnvironmentRequest {
1796            reality_config: None,
1797            chaos_config: None,
1798            drift_budget_config: None,
1799        };
1800
1801        assert!(request.reality_config.is_none());
1802    }
1803
1804    #[test]
1805    fn test_update_mock_environment_request_with_configs() {
1806        let request = UpdateMockEnvironmentRequest {
1807            reality_config: Some(serde_json::json!({"level": "medium"})),
1808            chaos_config: Some(serde_json::json!({"rate": 0.5})),
1809            drift_budget_config: None,
1810        };
1811
1812        assert!(request.reality_config.is_some());
1813        assert!(request.chaos_config.is_some());
1814    }
1815
1816    // ==================== Handler Tests ====================
1817
1818    #[tokio::test]
1819    async fn test_create_workspace() {
1820        let state = create_test_state();
1821
1822        let request = CreateWorkspaceRequest {
1823            id: "test".to_string(),
1824            name: "Test Workspace".to_string(),
1825            description: Some("Test description".to_string()),
1826        };
1827
1828        let result = create_workspace(State(state.clone()), Json(request)).await.unwrap();
1829
1830        assert!(result.0.success);
1831        assert_eq!(result.0.data.as_ref().unwrap().id, "test");
1832    }
1833
1834    #[tokio::test]
1835    async fn test_list_workspaces() {
1836        let state = create_test_state();
1837
1838        // Create a workspace first
1839        let request = CreateWorkspaceRequest {
1840            id: "test".to_string(),
1841            name: "Test Workspace".to_string(),
1842            description: None,
1843        };
1844
1845        let _ = create_workspace(State(state.clone()), Json(request)).await;
1846
1847        let result = list_workspaces(State(state)).await.unwrap();
1848
1849        assert!(result.0.success);
1850        assert!(!result.0.data.unwrap().is_empty());
1851    }
1852
1853    #[tokio::test]
1854    async fn test_get_workspace() {
1855        let state = create_test_state();
1856
1857        // Create a workspace first
1858        let request = CreateWorkspaceRequest {
1859            id: "get-test".to_string(),
1860            name: "Get Test Workspace".to_string(),
1861            description: None,
1862        };
1863
1864        let _ = create_workspace(State(state.clone()), Json(request)).await;
1865
1866        let result = get_workspace(State(state), Path("get-test".to_string())).await.unwrap();
1867
1868        assert!(result.0.success);
1869        assert_eq!(result.0.data.as_ref().unwrap().id, "get-test");
1870    }
1871
1872    #[tokio::test]
1873    async fn test_get_workspace_not_found() {
1874        let state = create_test_state();
1875
1876        let result = get_workspace(State(state), Path("nonexistent".to_string())).await;
1877
1878        assert!(result.is_err());
1879    }
1880
1881    #[tokio::test]
1882    async fn test_create_duplicate_workspace() {
1883        let state = create_test_state();
1884
1885        let request = CreateWorkspaceRequest {
1886            id: "duplicate".to_string(),
1887            name: "First".to_string(),
1888            description: None,
1889        };
1890
1891        let _ = create_workspace(State(state.clone()), Json(request)).await;
1892
1893        let request2 = CreateWorkspaceRequest {
1894            id: "duplicate".to_string(),
1895            name: "Second".to_string(),
1896            description: None,
1897        };
1898
1899        let result = create_workspace(State(state), Json(request2)).await;
1900        assert!(result.is_err());
1901    }
1902
1903    #[tokio::test]
1904    async fn test_delete_workspace() {
1905        let state = create_test_state();
1906
1907        // Create a workspace first
1908        let request = CreateWorkspaceRequest {
1909            id: "delete-test".to_string(),
1910            name: "Delete Test".to_string(),
1911            description: None,
1912        };
1913
1914        let _ = create_workspace(State(state.clone()), Json(request)).await;
1915
1916        let result = delete_workspace(State(state.clone()), Path("delete-test".to_string())).await;
1917
1918        assert!(result.is_ok());
1919        assert!(result.unwrap().0.success);
1920
1921        // Verify workspace is gone
1922        let get_result = get_workspace(State(state), Path("delete-test".to_string())).await;
1923        assert!(get_result.is_err());
1924    }
1925
1926    #[tokio::test]
1927    async fn test_update_workspace() {
1928        let state = create_test_state();
1929
1930        // Create a workspace first
1931        let create_request = CreateWorkspaceRequest {
1932            id: "update-test".to_string(),
1933            name: "Original Name".to_string(),
1934            description: None,
1935        };
1936
1937        let _ = create_workspace(State(state.clone()), Json(create_request)).await;
1938
1939        // Update the workspace
1940        let update_request = UpdateWorkspaceRequest {
1941            name: Some("Updated Name".to_string()),
1942            description: Some("New description".to_string()),
1943            enabled: Some(false),
1944        };
1945
1946        let result = update_workspace(
1947            State(state.clone()),
1948            Path("update-test".to_string()),
1949            Json(update_request),
1950        )
1951        .await;
1952
1953        assert!(result.is_ok());
1954        let response = result.unwrap();
1955        assert!(response.0.success);
1956        assert_eq!(response.0.data.as_ref().unwrap().name, "Updated Name");
1957    }
1958
1959    #[tokio::test]
1960    async fn test_get_workspace_stats() {
1961        let state = create_test_state();
1962
1963        // Create a workspace first
1964        let request = CreateWorkspaceRequest {
1965            id: "stats-test".to_string(),
1966            name: "Stats Test".to_string(),
1967            description: None,
1968        };
1969
1970        let _ = create_workspace(State(state.clone()), Json(request)).await;
1971
1972        let result = get_workspace_stats(State(state), Path("stats-test".to_string())).await;
1973
1974        assert!(result.is_ok());
1975        assert!(result.unwrap().0.success);
1976    }
1977}