use tempfile::tempdir;
use tokio::fs;
use std::{path::{Path, PathBuf}, sync::Arc};
use markhor_core::storage::{
self, ConflictError, Document, Error, Folder, Storage, Workspace, INTERNAL_DIR_NAME, MARKHOR_EXTENSION
};
async fn create_dummy(path: &Path, is_dir: bool) {
if is_dir {
fs::create_dir_all(path).await.expect("Test helper: Failed to create dummy dir");
} else {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await.expect("Test helper: Failed to create parent dir");
}
fs::write(path, "").await.expect("Test helper: Failed to create dummy file");
}
}
#[tokio::test]
async fn integration_create_and_open_workspace() {
let dir = tempdir().unwrap();
let ws_path = dir.path().join("my_integration_ws");
let storage = Arc::new(Storage::new());
let created_ws = Workspace::create(&storage, &*ws_path).await.expect("Failed to create workspace");
let absolute_path = fs::canonicalize(&ws_path).await.unwrap(); assert_eq!(created_ws.path(), absolute_path, "Workspace path should match the canonicalized path");
assert!(created_ws.path().join(INTERNAL_DIR_NAME).exists(), "Internal .markhor directory should exist after create");
assert!(created_ws.path().join(INTERNAL_DIR_NAME).is_dir(), "Internal .markhor should be a directory");
let opened_ws = Workspace::open(&storage, &*ws_path).await.expect("Failed to open existing workspace");
assert_eq!(opened_ws.path(), absolute_path);
assert!(opened_ws.path().join(INTERNAL_DIR_NAME).exists(), "Internal .markhor directory should exist after open");
let non_existent_path = dir.path().join("non_existent_ws");
let open_err = Workspace::open(&storage, &*non_existent_path).await;
assert!(matches!(open_err, Err(Error::DirectoryNotFound(_))), "Opening non-existent path should fail");
let not_a_ws_path = dir.path().join("not_a_ws");
create_dummy(¬_a_ws_path, true).await; let open_err_2 = Workspace::open(&storage, &*not_a_ws_path).await;
assert!(matches!(open_err_2, Err(Error::NotAWorkspace(_))), "Opening dir without .markhor should fail");
}
#[tokio::test]
async fn integration_folders_and_nested_docs() {
let dir = tempdir().unwrap();
let storage = Arc::new(Storage::new());
let ws = Workspace::create(&storage, dir.path()).await.unwrap();
let folder1_path = ws.path().join("FolderA");
let folder2_path = folder1_path.join("FolderB");
create_dummy(&folder1_path, true).await;
create_dummy(&folder2_path, true).await;
let root_folders = ws.root().await.list_folders().await.unwrap();
assert_eq!(root_folders.len(), 1);
assert_eq!(root_folders[0].path(), PathBuf::from("FolderA"));
let folder_a = ws.root().await.list_folders().await.unwrap().into_iter()
.find(|f| f.path() == PathBuf::from("FolderA"))
.expect("Could not find FolderA");
let docs_in_a = folder_a.list_documents().await.unwrap();
assert_eq!(docs_in_a.len(), 0);
let _doc1 = folder_a.create_document("doc_in_a").await.unwrap();
let docs_in_a = folder_a.list_documents().await.unwrap();
assert_eq!(docs_in_a.len(), 1);
assert_eq!(docs_in_a[0].path(), PathBuf::from("FolderA").join("doc_in_a.markhor"));
let folders_in_a = folder_a.list_folders().await.unwrap();
assert_eq!(folders_in_a.len(), 1);
assert_eq!(folders_in_a[0].path(), PathBuf::from("FolderA").join("FolderB"));
let folder_b = folders_in_a.into_iter().next().expect("Could not find FolderB");
let docs_in_b = folder_b.list_documents().await.unwrap();
assert_eq!(docs_in_b.len(), 0);
let _doc2 = folder_b.create_document("doc_in_b").await.unwrap();
let docs_in_b = folder_b.list_documents().await.unwrap();
assert_eq!(docs_in_b.len(), 1);
assert_eq!(docs_in_b[0].path(), PathBuf::from("FolderA").join("FolderB").join("doc_in_b.markhor"));
let folders_in_b = folder_b.list_folders().await.unwrap();
assert!(folders_in_b.is_empty());
}
#[tokio::test]
async fn integration_move_document_between_folders() {
let dir = tempdir().unwrap();
let storage = Arc::new(Storage::new());
let ws = Workspace::create(&storage, dir.path()).await.unwrap();
let folder_a = ws.root().await.create_subfolder("DirA").await.unwrap();
let folder_b = ws.root().await.create_subfolder("DirB").await.unwrap();
let original_doc_path = ws.path().join(folder_a.path()).join("movable.markhor");
let original_file_path = ws.path().join(folder_a.path()).join("movable.data");
let target_doc_path = ws.path().join(folder_b.path()).join("moved_doc.markhor"); let target_file_path = ws.path().join(folder_b.path()).join("moved_doc.data");
create_dummy(&ws.path().join(folder_a.path()), true).await;
create_dummy(&ws.path().join(folder_b.path()), true).await;
let doc = folder_a.create_document("movable").await.unwrap();
create_dummy(&original_file_path, false).await;
assert!(original_doc_path.exists());
assert!(original_file_path.exists());
assert!(!target_doc_path.exists());
assert!(!folder_b.path().join("moved_doc.data").exists());
let moved_doc = doc.move_to(target_doc_path.clone()).await.unwrap();
assert!(!original_doc_path.exists(), "Original .markhor should be gone");
assert!(!original_file_path.exists(), "Original .data file should be gone");
assert!(target_doc_path.exists(), "Target .markhor should exist");
assert!(target_file_path.exists(), "Target .data file should exist");
assert_eq!(moved_doc.path(), folder_b.path().join("moved_doc.markhor"), "Moved document internal path should be updated");
let folders = ws.root().await.list_folders().await.unwrap();
let folder_b = folders.iter().find(|f| f.path() == folder_b.path()).unwrap();
let docs_in_b = folder_b.list_documents().await.unwrap();
assert_eq!(docs_in_b.len(), 1);
assert_eq!(docs_in_b[0].path(), folder_b.path().join("moved_doc.markhor"));
let folder_a = folders.iter().find(|f| f.path() == folder_a.path()).unwrap();
let docs_in_a = folder_a.list_documents().await.unwrap();
assert!(docs_in_a.is_empty());
}
#[tokio::test]
async fn integration_move_document_causes_conflict() {
let dir = tempdir().unwrap();
let storage = Arc::new(Storage::new());
let ws = Workspace::create(&storage, dir.path()).await.unwrap();
let doc1_path = ws.path().join("doc1.markhor");
let doc2_path = ws.path().join("doc2.markhor");
let conflicting_file_for_doc1 = ws.path().join("doc1.txt");
let doc1 = ws.root().await.create_document("doc1").await.unwrap();
let doc2 = ws.root().await.create_document("doc2").await.unwrap();
create_dummy(&conflicting_file_for_doc1, false).await;
let move_result = doc2.move_to(doc1_path.clone()).await;
assert!(matches!(move_result, Err(Error::Conflict(ConflictError::MarkhorFileExists(p))) if p == doc1_path));
let conflicting_file_for_doc2 = ws.path().join("doc2.txt");
create_dummy(&conflicting_file_for_doc2, false).await;
let move_result_2 = doc1.move_to(doc2_path.clone()).await;
assert!(matches!(move_result_2, Err(Error::Conflict(ConflictError::MarkhorFileExists(p))) if p == doc2_path));
}
#[tokio::test]
async fn integration_move_adopts_orphan_conflict() {
let dir = tempdir().unwrap();
let storage = Arc::new(Storage::new());
let ws = Workspace::create(&storage, dir.path()).await.unwrap();
let source_doc_path = ws.path().join("source.markhor");
let target_doc_path = ws.path().join("target.markhor"); let orphan_file_path = ws.path().join("target.txt");
let source_doc = ws.root().await.create_document("source").await.unwrap();
create_dummy(&orphan_file_path, false).await;
assert!(source_doc_path.exists());
assert!(!target_doc_path.exists()); assert!(orphan_file_path.exists());
let move_result = source_doc.move_to(target_doc_path.clone()).await;
let err_msg = format!(
"Move should fail with ExistingFileWouldBeAdopted, but got: {:?}",
move_result
);
assert!(
matches!(move_result, Err(Error::Conflict(ConflictError::ExistingFileWouldBeAdopted(p))) if p == orphan_file_path),
"{}", err_msg
);
assert!(source_doc_path.exists(), "Source document should still exist after failed move");
assert!(!target_doc_path.exists(), "Target document should not have been created");
assert!(orphan_file_path.exists(), "Orphan file should still exist");
}
#[tokio::test]
async fn integration_create_document_causes_conflict() {
let dir = tempdir().unwrap();
let storage = Arc::new(Storage::new());
let ws = Workspace::create(&storage, dir.path()).await.unwrap();
let base_doc_name = "base";
let suffix_doc_name = "base.a1";
let conflicting_file_path = ws.path().join("base.txt");
create_dummy(&conflicting_file_path, false).await;
let create_result_1 = ws.root().await.create_document(base_doc_name).await;
assert!(matches!(create_result_1, Err(Error::Conflict(ConflictError::ExistingFileWouldBeAdopted(p))) if p == conflicting_file_path));
fs::remove_file(&conflicting_file_path).await.unwrap();
let base_doc = ws.root().await.create_document(base_doc_name).await.unwrap();
let create_result_2 = ws.root().await.create_document(suffix_doc_name).await;
assert!(matches!(create_result_2, Err(Error::Conflict(ConflictError::SuffixBaseAmbiguity(b, s))) if b == "base" && s == "a1"));
base_doc.delete().await.unwrap();
let _suffix_doc = ws.root().await.create_document(suffix_doc_name).await;
let create_result_3 = ws.root().await.create_document(base_doc_name).await;
assert!(matches!(create_result_3, Err(Error::Conflict(ConflictError::BaseSuffixAmbiguity(b, s))) if b == "base" && s == "base.a1"));
}