//! Integration tests for file move operations
//!
//! This test suite verifies file move handling in four scenarios:
//! 1. Persistent + Manual Reindex (batch change detection)
//! 2. Persistent + Watcher (real-time change handling)
//! 3. Ephemeral + Manual Reindex (batch change detection)
//! 4. Ephemeral + Watcher (real-time change handling)
//!
//! Each test validates:
//! - File UUID/inode preservation
//! - Parent-child relationship updates
//! - Correct event emission for UI updates

mod helpers;

use helpers::*;
use sd_core::{
	domain::addressing::SdPath,
	location::IndexMode,
	ops::indexing::{IndexScope, IndexerJob, IndexerJobConfig},
};
use tokio::time::Duration;

// ============================================================================
// PERSISTENT INDEXING TESTS
// ============================================================================

#[tokio::test]
async fn test_persistent_file_move_via_reindex() -> anyhow::Result<()> {
	// Tests batch change detection during manual reindex (watcher disabled)
	let harness = IndexingHarnessBuilder::new("persistent_move_reindex")
		.disable_watcher()
		.build()
		.await?;

	let test_location = harness.create_test_location("test_move").await?;

	// Create folder structure with files
	test_location.create_dir("source_folder").await?;
	test_location.create_dir("destination_folder").await?;
	test_location
		.write_file("source_folder/file1.txt", "Content 1")
		.await?;
	test_location
		.write_file("source_folder/file2.rs", "fn main() {}")
		.await?;
	test_location
		.write_file("destination_folder/existing.md", "# Existing")
		.await?;

	// Initial indexing
	let location = test_location
		.index("Test Location", IndexMode::Shallow)
		.await?;

	// Verify initial structure
	let initial_entries = location.get_all_entries().await?;

	let source_folder = initial_entries
		.iter()
		.find(|e| e.name == "source_folder" && e.kind == 1)
		.expect("Source folder should exist");
	let source_folder_id = source_folder.id;

	let dest_folder = initial_entries
		.iter()
		.find(|e| e.name == "destination_folder" && e.kind == 1)
		.expect("Destination folder should exist");
	let dest_folder_id = dest_folder.id;

	let file1_before = initial_entries
		.iter()
		.find(|e| e.name == "file1")
		.expect("file1 should exist");
	let file1_uuid = file1_before.uuid;
	let file1_inode = file1_before.inode;
	let file1_id = file1_before.id;

	assert_eq!(
		file1_before.parent_id,
		Some(source_folder_id),
		"file1 should be child of source_folder"
	);

	let initial_file_count = location.count_files().await?;
	assert_eq!(initial_file_count, 3, "Should have 3 files initially");

	// Wait for indexing to settle
	tokio::time::sleep(Duration::from_millis(500)).await;

	// Start collecting events BEFORE move
	let mut collector = EventCollector::new(&harness.core.events);
	let collection_handle = tokio::spawn(async move {
		collector.collect_events(Duration::from_secs(5)).await;
		collector
	});

	tokio::time::sleep(Duration::from_millis(100)).await;

	// Move the file from source to destination
	tracing::info!("Moving file from source_folder to destination_folder");
	location
		.move_file("source_folder/file1.txt", "destination_folder/file1.txt")
		.await?;

	// Manual reindex to detect the move via batch change detection
	location.reindex().await?;

	// Wait for reindex to complete
	tokio::time::sleep(Duration::from_secs(2)).await;

	// Verify final structure
	let final_entries = location.get_all_entries().await?;

	// File should have same UUID/inode/ID but new parent
	let file1_after = final_entries
		.iter()
		.find(|e| e.name == "file1")
		.expect("file1 should exist after move");

	assert_eq!(
		file1_after.uuid, file1_uuid,
		"File UUID should be preserved after move"
	);
	assert_eq!(
		file1_after.inode, file1_inode,
		"File inode should be preserved after move"
	);
	assert_eq!(
		file1_after.id, file1_id,
		"File ID should be preserved after move"
	);
	assert_eq!(
		file1_after.parent_id,
		Some(dest_folder_id),
		"file1 should now be child of destination_folder"
	);

	// file2 should still be in source folder
	let file2_after = final_entries
		.iter()
		.find(|e| e.name == "file2")
		.expect("file2 should still exist");
	assert_eq!(
		file2_after.parent_id,
		Some(source_folder_id),
		"file2 should still be in source_folder"
	);

	// Total file count should remain the same
	let final_file_count = location.count_files().await?;
	assert_eq!(
		final_file_count, initial_file_count,
		"File count should remain the same after move"
	);

	// CRITICAL: Verify closure table integrity
	location.verify_closure_table_integrity().await?;

	// Verify events were emitted during the operation
	let collector = collection_handle.await.unwrap();
	let stats = collector.analyze().await;

	let total_events = stats.resource_changed_batch.values().sum::<usize>()
		+ stats.resource_changed.values().sum::<usize>();

	if total_events == 0 {
		tracing::warn!("No resource change events emitted during file move");
	}

	harness.shutdown().await?;
	Ok(())
}

#[tokio::test]
async fn test_persistent_file_move_via_watcher() -> anyhow::Result<()> {
	// Tests real-time watcher change handling (no manual reindex)
	let harness = IndexingHarnessBuilder::new("persistent_move_watcher")
		.build() // Watcher enabled by default
		.await?;

	let test_location = harness.create_test_location("test_move").await?;

	// Create folder structure with files
	test_location.create_dir("source_folder").await?;
	test_location.create_dir("destination_folder").await?;
	test_location
		.write_file("source_folder/file1.txt", "Content 1")
		.await?;
	test_location
		.write_file("source_folder/file2.rs", "fn main() {}")
		.await?;

	// Initial indexing
	let location = test_location
		.index("Test Location", IndexMode::Shallow)
		.await?;

	let initial_entries = location.get_all_entries().await?;

	let dest_folder = initial_entries
		.iter()
		.find(|e| e.name == "destination_folder" && e.kind == 1)
		.expect("Destination folder should exist");
	let dest_folder_id = dest_folder.id;

	let file1_before = initial_entries
		.iter()
		.find(|e| e.name == "file1")
		.expect("file1 should exist");
	let file1_uuid = file1_before.uuid;
	let file1_id = file1_before.id;

	// Wait for indexing to settle
	tokio::time::sleep(Duration::from_millis(500)).await;

	// Start collecting events
	let mut collector = EventCollector::new(&harness.core.events);
	let collection_handle = tokio::spawn(async move {
		collector.collect_events(Duration::from_secs(10)).await;
		collector
	});

	tokio::time::sleep(Duration::from_millis(100)).await;

	// Move the file - watcher should detect and handle it
	tracing::info!("Moving file (watcher will detect)");
	location
		.move_file("source_folder/file1.txt", "destination_folder/file1.txt")
		.await?;

	// NO manual reindex - rely on watcher to handle the change
	// Wait for watcher to process the move event
	tokio::time::sleep(Duration::from_secs(8)).await;

	// Verify final structure
	let final_entries = location.get_all_entries().await?;

	// File should exist with same UUID/ID but new parent
	let file1_after = final_entries
		.iter()
		.find(|e| e.name == "file1")
		.expect("file1 should exist after move");

	// Watcher should preserve UUID/ID through move detection
	assert_eq!(
		file1_after.uuid, file1_uuid,
		"File UUID should be preserved by watcher"
	);
	assert_eq!(
		file1_after.id, file1_id,
		"File ID should be preserved by watcher"
	);
	assert_eq!(
		file1_after.parent_id,
		Some(dest_folder_id),
		"file1 should now be child of destination_folder"
	);

	// file2 should still exist in source folder
	let file2_exists = final_entries.iter().any(|e| e.name == "file2");
	assert!(file2_exists, "file2 should still exist");

	// CRITICAL: Verify closure table integrity
	location.verify_closure_table_integrity().await?;

	// Verify events were emitted (watcher emits ResourceChangedBatch for persistent)
	let collector = collection_handle.await.unwrap();
	let stats = collector.analyze().await;

	let event_count = stats.resource_changed_batch.values().sum::<usize>()
		+ stats.resource_changed.values().sum::<usize>();
	assert!(
		event_count > 0,
		"Should emit resource change events from watcher"
	);

	harness.shutdown().await?;
	Ok(())
}

// ============================================================================
// EPHEMERAL INDEXING TESTS
// ============================================================================

#[tokio::test]
async fn test_ephemeral_file_move_via_reindex() -> anyhow::Result<()> {
	// Tests ephemeral batch change detection during manual reindex (watcher disabled)
	let harness = IndexingHarnessBuilder::new("ephemeral_move_reindex")
		.disable_watcher()
		.build()
		.await?;

	let test_root = harness.temp_path();
	let source_folder = test_root.join("source_folder");
	let dest_folder = test_root.join("destination_folder");

	tokio::fs::create_dir_all(&source_folder).await?;
	tokio::fs::create_dir_all(&dest_folder).await?;

	tokio::fs::write(source_folder.join("file1.txt"), "Content 1").await?;
	tokio::fs::write(source_folder.join("file2.rs"), "fn main() {}").await?;

	// Index in ephemeral mode
	let test_root_sd = SdPath::local(test_root.clone());
	let indexer_config =
		IndexerJobConfig::ephemeral_browse(test_root_sd, IndexScope::Recursive, false);
	let indexer_job = IndexerJob::new(indexer_config);

	tracing::info!("Initial ephemeral indexing");
	let index_handle = harness.library.jobs().dispatch(indexer_job).await?;
	index_handle.wait().await?;

	harness
		.core
		.context
		.ephemeral_cache()
		.mark_indexing_complete(&test_root);

	tokio::time::sleep(Duration::from_millis(500)).await;

	// Move the file
	tracing::info!("Moving file in filesystem");
	tokio::fs::rename(
		source_folder.join("file1.txt"),
		dest_folder.join("file1.txt"),
	)
	.await?;

	// Manual reindex to detect the change
	let reindex_config = IndexerJobConfig::ephemeral_browse(
		SdPath::local(test_root.clone()),
		IndexScope::Recursive,
		false,
	);
	let reindex_job = IndexerJob::new(reindex_config);
	let reindex_handle = harness.library.jobs().dispatch(reindex_job).await?;
	reindex_handle.wait().await?;

	tokio::time::sleep(Duration::from_millis(500)).await;

	// Verify filesystem state
	assert!(
		!tokio::fs::try_exists(source_folder.join("file1.txt"))
			.await
			.unwrap_or(false),
		"file1.txt should not exist in source folder"
	);
	assert!(
		tokio::fs::try_exists(dest_folder.join("file1.txt")).await?,
		"file1.txt should exist in destination folder"
	);
	assert!(
		tokio::fs::try_exists(source_folder.join("file2.rs")).await?,
		"file2.rs should still exist in source folder"
	);

	// Verify file content preserved
	let file_content = tokio::fs::read_to_string(dest_folder.join("file1.txt")).await?;
	assert_eq!(
		file_content, "Content 1",
		"File content should be preserved"
	);

	harness.shutdown().await?;
	Ok(())
}

#[tokio::test]
async fn test_ephemeral_file_move_via_watcher() -> anyhow::Result<()> {
	// Tests ephemeral real-time watcher change handling (no manual reindex)
	let harness = IndexingHarnessBuilder::new("ephemeral_move_watcher")
		.build() // Watcher enabled
		.await?;

	let test_root = harness.temp_path();
	let source_folder = test_root.join("source_folder");
	let dest_folder = test_root.join("destination_folder");

	tokio::fs::create_dir_all(&source_folder).await?;
	tokio::fs::create_dir_all(&dest_folder).await?;

	tokio::fs::write(source_folder.join("file1.txt"), "Content 1").await?;
	tokio::fs::write(source_folder.join("file2.rs"), "fn main() {}").await?;

	// Index in ephemeral mode
	let test_root_sd = SdPath::local(test_root.clone());
	let indexer_config =
		IndexerJobConfig::ephemeral_browse(test_root_sd, IndexScope::Recursive, false);
	let indexer_job = IndexerJob::new(indexer_config);

	tracing::info!("Initial ephemeral indexing");
	let index_handle = harness.library.jobs().dispatch(indexer_job).await?;
	index_handle.wait().await?;

	harness
		.core
		.context
		.ephemeral_cache()
		.mark_indexing_complete(&test_root);

	// Register for watching
	if let Some(watcher) = harness.core.context.get_fs_watcher().await {
		watcher.watch_ephemeral(test_root.clone()).await?;
		tokio::time::sleep(Duration::from_millis(500)).await;
	}

	// Start collecting events
	let mut collector = EventCollector::new(&harness.core.events);
	let collection_handle = tokio::spawn(async move {
		collector.collect_events(Duration::from_secs(15)).await;
		collector
	});

	tokio::time::sleep(Duration::from_millis(100)).await;

	// Move the file - watcher should detect it
	tracing::info!("Moving file (watcher will detect)");
	tokio::fs::rename(
		source_folder.join("file1.txt"),
		dest_folder.join("file1.txt"),
	)
	.await?;

	// NO manual reindex - wait for watcher to handle it
	tokio::time::sleep(Duration::from_secs(12)).await;

	// Verify filesystem state
	assert!(
		!tokio::fs::try_exists(source_folder.join("file1.txt"))
			.await
			.unwrap_or(false),
		"file1.txt should not exist in source folder"
	);
	assert!(
		tokio::fs::try_exists(dest_folder.join("file1.txt")).await?,
		"file1.txt should exist in destination folder"
	);
	assert!(
		tokio::fs::try_exists(source_folder.join("file2.rs")).await?,
		"file2.rs should still exist in source folder"
	);

	// Verify file content preserved
	let file_content = tokio::fs::read_to_string(dest_folder.join("file1.txt")).await?;
	assert_eq!(
		file_content, "Content 1",
		"File content should be preserved"
	);

	// Verify watcher emitted events (ephemeral uses individual ResourceChanged events)
	let collector = collection_handle.await.unwrap();
	let stats = collector.analyze().await;

	let event_count = stats.resource_changed.values().sum::<usize>();
	if event_count == 0 {
		tracing::warn!("No ResourceChanged events emitted - ephemeral watcher may not emit events for file moves in test environment (works in prod)");
	}

	harness.shutdown().await?;
	Ok(())
}
