use crate::db::Database;
use crate::db::models::Session;
use crate::db::repository::session::SessionRepository;
use crate::services::ServiceContext;
use crate::services::plan::*;
use crate::tui::plan::PlanDocument;
use crate::tui::plan::{PlanStatus, PlanTask, TaskStatus, TaskType};
use tempfile::TempDir;
use tokio;
use uuid::Uuid;
async fn setup_test_service() -> (Database, PlanService, Session, TempDir) {
let db = Database::connect_in_memory()
.await
.expect("Failed to create database");
db.run_migrations().await.expect("Failed to run migrations");
let context = ServiceContext::new(db.pool().clone());
let plan_service = PlanService::new(context.clone());
let session_repo = SessionRepository::new(db.pool().clone());
let session = Session::new(
Some("Test Session".to_string()),
Some("claude-sonnet-4-5".to_string()),
None,
);
session_repo
.create(&session)
.await
.expect("Failed to create test session");
let temp_dir = TempDir::new().expect("Failed to create temp dir");
(db, plan_service, session, temp_dir)
}
fn create_test_plan(session_id: Uuid) -> PlanDocument {
let mut plan = PlanDocument::new(
session_id,
"Test Plan".to_string(),
"A test plan for service testing".to_string(),
);
plan.context = "Test context".to_string();
plan.risks = vec!["Risk 1".to_string()];
let task = PlanTask {
id: Uuid::new_v4(),
order: 0,
title: "Task 1".to_string(),
description: "First task".to_string(),
task_type: TaskType::Research,
dependencies: vec![],
complexity: 3,
acceptance_criteria: vec![],
status: TaskStatus::Pending,
notes: None,
completed_at: None,
retry_count: 0,
max_retries: 3,
artifacts: Vec::new(),
};
plan.add_task(task);
plan
}
#[tokio::test]
async fn test_service_create_and_find() {
let (_db, service, session, _temp) = setup_test_service().await;
let plan = create_test_plan(session.id);
let plan_id = plan.id;
service.create(&plan).await.expect("Failed to create plan");
let found = service
.find_by_id(plan_id)
.await
.expect("Failed to find plan");
assert!(found.is_some());
assert_eq!(found.unwrap().title, "Test Plan");
}
#[tokio::test]
async fn test_service_update() {
let (_db, service, session, _temp) = setup_test_service().await;
let mut plan = create_test_plan(session.id);
service.create(&plan).await.expect("Failed to create plan");
plan.title = "Updated Title".to_string();
plan.status = PlanStatus::Approved;
service.update(&plan).await.expect("Failed to update plan");
let found = service
.find_by_id(plan.id)
.await
.expect("Failed to find")
.unwrap();
assert_eq!(found.title, "Updated Title");
assert_eq!(found.status, PlanStatus::Approved);
}
#[tokio::test]
async fn test_service_delete() {
let (_db, service, session, _temp) = setup_test_service().await;
let plan = create_test_plan(session.id);
service.create(&plan).await.expect("Failed to create plan");
service
.delete(plan.id)
.await
.expect("Failed to delete plan");
let found = service.find_by_id(plan.id).await.expect("Failed to query");
assert!(found.is_none());
}
#[tokio::test]
async fn test_service_find_by_session_id() {
let (_db, service, session, _temp) = setup_test_service().await;
let plan1 = create_test_plan(session.id);
let plan2 = create_test_plan(session.id);
service
.create(&plan1)
.await
.expect("Failed to create plan1");
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await; service
.create(&plan2)
.await
.expect("Failed to create plan2");
let plans = service
.find_by_session_id(session.id)
.await
.expect("Failed to find plans");
assert_eq!(plans.len(), 2);
}
#[tokio::test]
async fn test_service_get_most_recent_plan() {
let (_db, service, session, _temp) = setup_test_service().await;
let recent = service
.get_most_recent_plan(session.id)
.await
.expect("Failed to get recent plan");
assert!(recent.is_none());
let plan1 = create_test_plan(session.id);
let plan1_id = plan1.id;
service
.create(&plan1)
.await
.expect("Failed to create plan1");
let recent = service
.get_most_recent_plan(session.id)
.await
.expect("Failed to get recent plan");
assert!(recent.is_some());
assert_eq!(recent.unwrap().id, plan1_id);
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let plan2 = create_test_plan(session.id);
service
.create(&plan2)
.await
.expect("Failed to create plan2");
let recent = service
.get_most_recent_plan(session.id)
.await
.expect("Failed to get recent plan");
assert!(recent.is_some());
let recent_plan = recent.unwrap();
assert!(recent_plan.id == plan1_id || recent_plan.id == plan2.id);
}
#[tokio::test]
async fn test_service_export_to_json() {
let (_db, service, session, temp) = setup_test_service().await;
let plan = create_test_plan(session.id);
service.create(&plan).await.expect("Failed to create plan");
let json_path = temp.path().join("test_plan.json");
service
.export_to_json(&plan, &json_path)
.await
.expect("Failed to export to JSON");
assert!(json_path.exists());
let content = std::fs::read_to_string(&json_path).expect("Failed to read JSON file");
let parsed: PlanDocument = serde_json::from_str(&content).expect("Invalid JSON");
assert_eq!(parsed.id, plan.id);
assert_eq!(parsed.title, plan.title);
}
#[tokio::test]
async fn test_service_import_from_json() {
let (_db, service, session, temp) = setup_test_service().await;
let plan = create_test_plan(session.id);
let json_path = temp.path().join("test_plan.json");
let json = serde_json::to_string_pretty(&plan).expect("Failed to serialize");
std::fs::write(&json_path, json).expect("Failed to write JSON file");
let imported = service
.import_from_json(&json_path)
.await
.expect("Failed to import from JSON");
assert_eq!(imported.id, plan.id);
assert_eq!(imported.title, plan.title);
assert_eq!(imported.tasks.len(), plan.tasks.len());
}
#[tokio::test]
async fn test_service_export_import_roundtrip() {
let (_db, service, session, temp) = setup_test_service().await;
let original_plan = create_test_plan(session.id);
service
.create(&original_plan)
.await
.expect("Failed to create plan");
let json_path = temp.path().join("roundtrip.json");
service
.export_to_json(&original_plan, &json_path)
.await
.expect("Failed to export");
let imported_plan = service
.import_from_json(&json_path)
.await
.expect("Failed to import");
assert_eq!(imported_plan.id, original_plan.id);
assert_eq!(imported_plan.session_id, original_plan.session_id);
assert_eq!(imported_plan.title, original_plan.title);
assert_eq!(imported_plan.description, original_plan.description);
assert_eq!(imported_plan.context, original_plan.context);
assert_eq!(imported_plan.risks, original_plan.risks);
assert_eq!(imported_plan.status, original_plan.status);
assert_eq!(imported_plan.tasks.len(), original_plan.tasks.len());
if let (Some(orig_task), Some(imp_task)) =
(imported_plan.tasks.first(), original_plan.tasks.first())
{
assert_eq!(orig_task.id, imp_task.id);
assert_eq!(orig_task.title, imp_task.title);
assert_eq!(orig_task.task_type, imp_task.task_type);
}
}
#[tokio::test]
async fn test_service_atomic_json_write() {
let (_db, service, session, temp) = setup_test_service().await;
let plan = create_test_plan(session.id);
let json_path = temp.path().join("atomic.json");
service
.export_to_json(&plan, &json_path)
.await
.expect("Failed to export");
let temp_file = temp.path().join("atomic.tmp");
assert!(!temp_file.exists());
assert!(json_path.exists());
}
#[tokio::test]
async fn test_service_json_import_nonexistent_file() {
let (_db, service, _session, temp) = setup_test_service().await;
let json_path = temp.path().join("nonexistent.json");
let result = service.import_from_json(&json_path).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_service_json_import_invalid_json() {
let (_db, service, _session, temp) = setup_test_service().await;
let json_path = temp.path().join("invalid.json");
std::fs::write(&json_path, "{ invalid json }").expect("Failed to write file");
let result = service.import_from_json(&json_path).await;
assert!(result.is_err());
}