use axum_typed_multipart::FieldData;
use bytes;
use mime_guess::from_path;
use object_store::Error as ObjectStoreError;
use sha2::{Digest, Sha256};
use std::{
    io::{BufReader, Read, Seek, SeekFrom},
    path::Path,
};
use tempfile::NamedTempFile;
use thiserror::Error;
use tokio::task;
use tracing::info;
use uuid::Uuid;

use crate::{
    error::AppError,
    storage::{db::SurrealDbClient, store, store::StorageManager},
    stored_object,
};

/// Errors that can occur during file storage operations.
#[derive(Error, Debug)]
pub enum FileError {
    /// No file record found with the given UUID.
    #[error("file not found for uuid: {0}")]
    FileNotFound(String),

    /// Underlying I/O operation failed.
    #[error("io error occurred: {0}")]
    Io(#[from] std::io::Error),

    /// A file with the same SHA-256 hash already exists.
    #[error("duplicate file detected with sha256: {0}")]
    DuplicateFile(String),

    /// Database operation on the file record failed.
    #[error("surrealdb error: {0}")]
    SurrealError(Box<surrealdb::Error>),

    /// Failed to persist the temporary file to its final location.
    #[error("failed to persist file: {0}")]
    PersistError(#[from] tempfile::PersistError),

    /// Upload metadata did not include a file name.
    #[error("file name missing in metadata")]
    MissingFileName,

    /// The underlying object store operation failed.
    #[error("object store error: {0}")]
    ObjectStore(Box<ObjectStoreError>),
}

impl From<surrealdb::Error> for FileError {
    fn from(err: surrealdb::Error) -> Self {
        Self::SurrealError(Box::new(err))
    }
}

impl From<ObjectStoreError> for FileError {
    fn from(err: ObjectStoreError) -> Self {
        Self::ObjectStore(Box::new(err))
    }
}

stored_object!(FileInfo, "file", {
    sha256: String,
    path: String,
    file_name: String,
    mime_type: String,
    user_id: String
});

impl FileInfo {
    /// Guesses the MIME type based on the file extension.
    ///
    /// # Arguments
    /// * `path` - The path to the file.
    ///
    /// # Returns
    /// * `String` - The guessed MIME type as a string.
    fn guess_mime_type(path: &Path) -> String {
        from_path(path)
            .first_or(mime::APPLICATION_OCTET_STREAM)
            .to_string()
    }

    fn sanitize_name_segment(segment: &str) -> String {
        segment
            .chars()
            .map(|c| {
                if c.is_ascii_alphanumeric() || c == '_' {
                    c
                } else {
                    '_'
                }
            })
            .collect()
    }

    /// Sanitizes the file name to prevent security vulnerabilities like directory traversal.
    /// Replaces any non-alphanumeric characters (excluding '.' and '_') with underscores in
    /// both the stem and extension.
    fn sanitize_file_name(file_name: &str) -> String {
        if let Some((stem, ext)) = file_name.rsplit_once('.') {
            let name = Self::sanitize_name_segment(stem);
            let ext = Self::sanitize_name_segment(ext);
            if ext.is_empty() {
                name
            } else {
                format!("{name}.{ext}")
            }
        } else {
            Self::sanitize_name_segment(file_name)
        }
    }

    /// Reads the file once and returns its SHA-256 hex digest and raw bytes.
    #[allow(clippy::indexing_slicing)]
    async fn read_and_hash(file: &NamedTempFile) -> Result<(String, Vec<u8>), FileError> {
        let mut file_clone = file.as_file().try_clone()?;

        task::spawn_blocking(move || -> Result<_, std::io::Error> {
            file_clone.seek(SeekFrom::Start(0))?;
            let mut reader = BufReader::new(&mut file_clone);
            let mut hasher = Sha256::new();
            let mut buffer = [0u8; 8192];
            let mut bytes = Vec::new();

            loop {
                let n = reader.read(&mut buffer)?;
                if n == 0 {
                    break;
                }
                hasher.update(&buffer[..n]);
                bytes.extend_from_slice(&buffer[..n]);
            }

            Ok((format!("{:x}", hasher.finalize()), bytes))
        })
        .await
        .map_err(std::io::Error::other)?
        .map_err(FileError::from)
    }

    /// Retrieves a `FileInfo` for this user by SHA256.
    async fn get_by_sha(
        sha256: &str,
        user_id: &str,
        db_client: &SurrealDbClient,
    ) -> Result<FileInfo, FileError> {
        let mut response = db_client
            .client
            .query("SELECT * FROM file WHERE sha256 = $sha256 AND user_id = $user_id LIMIT 1")
            .bind(("sha256", sha256.to_owned()))
            .bind(("user_id", user_id.to_owned()))
            .await?;
        let response: Vec<FileInfo> = response.take(0)?;

        response
            .into_iter()
            .next()
            .ok_or(FileError::FileNotFound(sha256.to_string()))
    }

    /// Retrieves a `FileInfo` by its ID.
    ///
    /// # Arguments
    /// * `id` - The ID string of the file.
    /// * `db_client` - Reference to the SurrealDbClient.
    ///
    /// # Returns
    /// * `Result<FileInfo, FileError>` - The `FileInfo` or an error if not found or on DB issues.
    pub async fn get_by_id(id: &str, db_client: &SurrealDbClient) -> Result<FileInfo, FileError> {
        match db_client.get_item::<FileInfo>(id).await {
            Ok(Some(file_info)) => Ok(file_info),
            Ok(None) => Err(FileError::FileNotFound(id.to_string())),
            Err(e) => Err(FileError::from(e)),
        }
    }

    /// Create a new FileInfo using StorageManager for persistent storage operations.
    ///
    /// # Arguments
    /// * `field_data` - The uploaded file data
    /// * `db_client` - Reference to the SurrealDbClient
    /// * `user_id` - The user ID
    /// * `storage` - A StorageManager instance for storage operations
    ///
    /// # Returns
    /// * `Result<Self, FileError>` - The created FileInfo or an error
    pub async fn new_with_storage(
        field_data: FieldData<NamedTempFile>,
        db_client: &SurrealDbClient,
        user_id: &str,
        storage: &StorageManager,
    ) -> Result<Self, FileError> {
        let file = field_data.contents;
        let file_name = field_data
            .metadata
            .file_name
            .ok_or(FileError::MissingFileName)?;
        let original_file_name = file_name.clone();

        let (sha256, bytes) = Self::read_and_hash(&file).await?;

        if let Ok(existing_file) = Self::get_by_sha(&sha256, user_id, db_client).await {
            info!(
                "File already exists for user {} with SHA256: {}",
                user_id, sha256
            );
            return Ok(existing_file);
        }

        let uuid = Uuid::new_v4();
        let sanitized_file_name = Self::sanitize_file_name(&file_name);
        let now = Utc::now();
        let storage_prefix = format!("{user_id}/{uuid}");

        let path =
            Self::persist_bytes_with_storage(&storage_prefix, &sanitized_file_name, bytes, storage)
                .await?;

        let file_info = FileInfo {
            id: uuid.to_string(),
            user_id: user_id.to_string(),
            sha256: sha256.clone(),
            file_name: original_file_name,
            path,
            mime_type: Self::guess_mime_type(Path::new(&file_name)),
            created_at: now,
            updated_at: now,
        };

        match db_client.store_item(file_info.clone()).await {
            Ok(_) => Ok(file_info),
            Err(e) => {
                if let Err(cleanup_err) = storage.delete_prefix(&storage_prefix).await {
                    tracing::warn!(
                        prefix = %storage_prefix,
                        error = %cleanup_err,
                        "failed to remove orphaned file after database error"
                    );
                }
                if let Ok(existing) = Self::get_by_sha(&sha256, user_id, db_client).await {
                    return Ok(existing);
                }
                Err(FileError::from(e))
            }
        }
    }

    /// Delete a FileInfo by ID using StorageManager for storage operations.
    ///
    /// # Arguments
    /// * `id` - ID of the FileInfo
    /// * `db_client` - Reference to SurrealDbClient
    /// * `storage` - A StorageManager instance for storage operations
    ///
    /// # Returns
    /// * `Result<(), AppError>` - Success or error
    pub async fn delete_by_id_with_storage(
        id: &str,
        db_client: &SurrealDbClient,
        storage: &StorageManager,
    ) -> Result<(), AppError> {
        // Get the FileInfo from the database
        let Some(file_info) = db_client.get_item::<FileInfo>(id).await? else {
            return Ok(());
        };

        // Remove the object's parent prefix in the object store
        let (parent_prefix, _file_name) =
            store::split_object_path(&file_info.path).map_err(AppError::internal)?;
        storage
            .delete_prefix(&parent_prefix)
            .await
            .map_err(AppError::from)?;
        info!(
            "Removed object prefix {} and its contents via StorageManager",
            parent_prefix
        );

        // Delete the FileInfo from the database
        db_client.delete_item::<FileInfo>(id).await?;

        Ok(())
    }

    /// Retrieve file content using StorageManager for storage operations.
    ///
    /// # Arguments
    /// * `storage` - A StorageManager instance for storage operations
    ///
    /// # Returns
    /// * `Result<bytes::Bytes, AppError>` - The file content or an error
    pub async fn get_content_with_storage(
        &self,
        storage: &StorageManager,
    ) -> Result<bytes::Bytes, AppError> {
        storage.get(&self.path).await.map_err(AppError::from)
    }

    /// Persist bytes to storage using StorageManager.
    async fn persist_bytes_with_storage(
        storage_prefix: &str,
        file_name: &str,
        bytes: Vec<u8>,
        storage: &StorageManager,
    ) -> Result<String, FileError> {
        let location = format!("{storage_prefix}/{file_name}");
        info!("Persisting to object location: {}", location);

        storage
            .put(&location, bytes.into())
            .await
            .map_err(FileError::from)?;

        Ok(location)
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used, clippy::must_use_candidate)]
    use anyhow::{self, Context};

    use super::*;
    use crate::storage::store::testing::TestStorageManager;
    use crate::test_utils::setup_test_db;
    use axum::http::HeaderMap;
    use axum_typed_multipart::FieldMetadata;
    use std::{io::Write, path::Path};
    use tempfile::NamedTempFile;

    /// Creates a test temporary file with the given content
    fn create_test_file(
        content: &[u8],
        file_name: &str,
    ) -> anyhow::Result<FieldData<NamedTempFile>> {
        let mut temp_file =
            NamedTempFile::new().with_context(|| "Failed to create temp file".to_string())?;
        temp_file
            .write_all(content)
            .with_context(|| "Failed to write to temp file".to_string())?;

        let metadata = FieldMetadata {
            name: Some("file".to_string()),
            file_name: Some(file_name.to_string()),
            content_type: None,
            headers: HeaderMap::default(),
        };

        Ok(FieldData {
            metadata,
            contents: temp_file,
        })
    }

    fn create_test_file_without_name(content: &[u8]) -> anyhow::Result<FieldData<NamedTempFile>> {
        let mut temp_file =
            NamedTempFile::new().with_context(|| "Failed to create temp file".to_string())?;
        temp_file
            .write_all(content)
            .with_context(|| "Failed to write to temp file".to_string())?;

        let metadata = FieldMetadata {
            name: Some("file".to_string()),
            file_name: None,
            content_type: None,
            headers: HeaderMap::default(),
        };

        Ok(FieldData {
            metadata,
            contents: temp_file,
        })
    }

    #[tokio::test]
    async fn test_fileinfo_create_read_delete_with_storage_manager() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let content = b"This is a test file for StorageManager operations";
        let file_name = "storage_manager_test.txt";
        let field_data = create_test_file(content, file_name)?;

        // Create test storage manager (memory backend)
        let test_storage = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "Failed to create test storage manager".to_string())?;

        // Create a FileInfo instance with storage manager
        let user_id = "test_user";

        // Test file creation with StorageManager
        let file_info =
            FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage())
                .await
                .with_context(|| "Failed to create file with StorageManager".to_string())?;
        assert_eq!(file_info.file_name, file_name);

        // Verify the file exists via StorageManager and has correct content
        let bytes = file_info
            .get_content_with_storage(test_storage.storage())
            .await
            .with_context(|| "Failed to read file content via StorageManager".to_string())?;
        assert_eq!(bytes.as_ref(), content);

        // Test file reading
        let retrieved = FileInfo::get_by_id(&file_info.id, &db)
            .await
            .with_context(|| "Failed to retrieve file info".to_string())?;
        assert_eq!(retrieved.id, file_info.id);
        assert_eq!(retrieved.sha256, file_info.sha256);
        assert_eq!(retrieved.file_name, file_name);

        // Test file deletion with StorageManager
        FileInfo::delete_by_id_with_storage(&file_info.id, &db, test_storage.storage())
            .await
            .with_context(|| "Failed to delete file with StorageManager".to_string())?;

        let deleted_result = file_info
            .get_content_with_storage(test_storage.storage())
            .await;
        assert!(deleted_result.is_err(), "File should be deleted");
        Ok(())
    }

    #[tokio::test]
    async fn test_fileinfo_preserves_original_filename_and_sanitizes_path() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let content = b"filename sanitization";
        let original_name = "Complex name (1).txt";
        let expected_sanitized = "Complex_name__1_.txt";
        let field_data = create_test_file(content, original_name)?;

        let test_storage = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "Failed to create test storage manager".to_string())?;

        let file_info =
            FileInfo::new_with_storage(field_data, &db, "sanitized_user", test_storage.storage())
                .await
                .with_context(|| "Failed to create file via storage manager".to_string())?;

        assert_eq!(file_info.file_name, original_name);

        let stored_name = Path::new(&file_info.path)
            .file_name()
            .and_then(|name| name.to_str())
            .with_context(|| "stored name".to_string())?;
        assert_eq!(stored_name, expected_sanitized);
        Ok(())
    }

    #[tokio::test]
    async fn test_fileinfo_duplicate_detection_with_storage_manager() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let content = b"This is a test file for StorageManager duplicate detection";
        let file_name = "storage_manager_duplicate.txt";
        let field_data = create_test_file(content, file_name)?;

        // Create test storage manager
        let test_storage = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "Failed to create test storage manager".to_string())?;

        // Create a FileInfo instance with storage manager
        let user_id = "test_user";

        // Store the original file
        let original_file_info =
            FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage())
                .await
                .with_context(|| {
                    "Failed to create original file with StorageManager".to_string()
                })?;

        // Create another file with the same content but different name
        let duplicate_name = "storage_manager_duplicate_2.txt";
        let field_data2 = create_test_file(content, duplicate_name)?;

        // The system should detect it's the same file and return the original FileInfo
        let duplicate_file_info =
            FileInfo::new_with_storage(field_data2, &db, user_id, test_storage.storage())
                .await
                .with_context(|| {
                    "Failed to process duplicate file with StorageManager".to_string()
                })?;

        // Verify duplicate detection worked
        assert_eq!(duplicate_file_info.id, original_file_info.id);
        assert_eq!(duplicate_file_info.sha256, original_file_info.sha256);
        assert_eq!(duplicate_file_info.file_name, file_name);
        assert_ne!(duplicate_file_info.file_name, duplicate_name);

        // Verify both files have the same content (they should point to the same file)
        let original_content = original_file_info
            .get_content_with_storage(test_storage.storage())
            .await
            .with_context(|| "get original content".to_string())?;
        let duplicate_content = duplicate_file_info
            .get_content_with_storage(test_storage.storage())
            .await
            .with_context(|| "get duplicate content".to_string())?;
        assert_eq!(original_content.as_ref(), content);
        assert_eq!(duplicate_content.as_ref(), content);

        // Clean up
        FileInfo::delete_by_id_with_storage(&original_file_info.id, &db, test_storage.storage())
            .await
            .with_context(|| "Failed to delete original file with StorageManager".to_string())?;
        Ok(())
    }

    #[tokio::test]
    async fn test_file_creation() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let content = b"This is a test file content";
        let file_name = "test_file.txt";
        let field_data = create_test_file(content, file_name)?;

        // Create a FileInfo instance with StorageManager
        let user_id = "test_user";
        let test_storage = TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage manager".to_string())?;
        let file_info =
            FileInfo::new_with_storage(field_data, &db, user_id, test_storage.storage()).await?;

        // Check essential properties
        assert!(!file_info.id.is_empty());
        assert_eq!(file_info.file_name, file_name);
        assert!(!file_info.sha256.is_empty());
        assert!(!file_info.path.is_empty());
        // path should be logical: "user_id/uuid/file_name"
        let parts: Vec<&str> = file_info.path.split('/').collect();
        assert_eq!(parts.len(), 3);
        assert_eq!(parts.first(), Some(&user_id));
        assert_eq!(parts.get(2), Some(&file_name));
        assert!(file_info.mime_type.contains("text/plain"));

        // Verify it's in the database
        let stored = db
            .get_item::<FileInfo>(&file_info.id)
            .await
            .with_context(|| "Failed to retrieve file info".to_string())?
            .with_context(|| "expected stored file".to_string())?;
        assert_eq!(stored.id, file_info.id);
        assert_eq!(stored.file_name, file_info.file_name);
        assert_eq!(stored.sha256, file_info.sha256);
        Ok(())
    }

    #[tokio::test]
    async fn test_file_duplicate_detection() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        // First, store a file with known content
        let content = b"This is a test file for duplicate detection";
        let file_name = "original.txt";
        let user_id = "test_user";

        let test_storage = TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage manager".to_string())?;

        let field_data1 = create_test_file(content, file_name)?;
        let original_file_info =
            FileInfo::new_with_storage(field_data1, &db, user_id, test_storage.storage())
                .await
                .with_context(|| "Failed to create original file".to_string())?;

        // Now try to store another file with the same content but different name
        let duplicate_name = "duplicate.txt";
        let field_data2 = create_test_file(content, duplicate_name)?;

        // The system should detect it's the same file and return the original FileInfo
        let duplicate_file_info =
            FileInfo::new_with_storage(field_data2, &db, user_id, test_storage.storage())
                .await
                .with_context(|| "Failed to process duplicate file".to_string())?;

        // The returned FileInfo should match the original
        assert_eq!(duplicate_file_info.id, original_file_info.id);
        assert_eq!(duplicate_file_info.sha256, original_file_info.sha256);

        // But it should retain the original file name, not the duplicate's name
        assert_eq!(duplicate_file_info.file_name, file_name);
        assert_ne!(duplicate_file_info.file_name, duplicate_name);
        Ok(())
    }

    #[tokio::test]
    async fn test_guess_mime_type() -> anyhow::Result<()> {
        // Test common file extensions
        assert_eq!(
            FileInfo::guess_mime_type(Path::new("test.txt")),
            "text/plain".to_string()
        );
        assert_eq!(
            FileInfo::guess_mime_type(Path::new("image.png")),
            "image/png".to_string()
        );
        assert_eq!(
            FileInfo::guess_mime_type(Path::new("document.pdf")),
            "application/pdf".to_string()
        );
        assert_eq!(
            FileInfo::guess_mime_type(Path::new("data.json")),
            "application/json".to_string()
        );

        // Test unknown extension
        assert_eq!(
            FileInfo::guess_mime_type(Path::new("unknown.929yz")),
            "application/octet-stream".to_string()
        );
        Ok(())
    }

    #[tokio::test]
    async fn test_sanitize_file_name() -> anyhow::Result<()> {
        // Safe characters should remain unchanged
        assert_eq!(
            FileInfo::sanitize_file_name("normal_file.txt"),
            "normal_file.txt"
        );
        assert_eq!(FileInfo::sanitize_file_name("file123.doc"), "file123.doc");

        // Unsafe characters should be replaced with underscores
        assert_eq!(
            FileInfo::sanitize_file_name("file with spaces.txt"),
            "file_with_spaces.txt"
        );
        assert_eq!(
            FileInfo::sanitize_file_name("file/with/path.txt"),
            "file_with_path.txt"
        );
        assert_eq!(
            FileInfo::sanitize_file_name("file:with:colons.txt"),
            "file_with_colons.txt"
        );
        assert_eq!(
            FileInfo::sanitize_file_name("../dangerous.txt"),
            "___dangerous.txt"
        );
        assert_eq!(
            FileInfo::sanitize_file_name("file.evil/../../x"),
            "file_evil_____._x"
        );
        Ok(())
    }

    #[tokio::test]
    async fn test_get_by_sha_not_found() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let result = FileInfo::get_by_sha("nonexistent_sha_hash", "user123", &db).await;
        assert!(result.is_err());

        match result {
            Err(FileError::FileNotFound(_)) => {}
            _ => anyhow::bail!("Expected FileNotFound error"),
        }
        Ok(())
    }

    #[tokio::test]
    async fn test_get_by_sha_resists_query_injection() {
        let db = setup_test_db().await.expect("setup test db");
        let now = Utc::now();
        let file_info = FileInfo {
            id: Uuid::new_v4().to_string(),
            created_at: now,
            updated_at: now,
            user_id: "user123".to_string(),
            sha256: "known_sha_value".to_string(),
            path: "/path/to/file.txt".to_string(),
            file_name: "file.txt".to_string(),
            mime_type: "text/plain".to_string(),
        };

        db.store_item(file_info)
            .await
            .expect("Failed to store test file info");

        let malicious_sha = "known_sha_value' OR true --";
        let result = FileInfo::get_by_sha(malicious_sha, "user123", &db).await;

        assert!(matches!(result, Err(FileError::FileNotFound(_))));
    }

    #[tokio::test]
    async fn test_duplicate_detection_is_per_user() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let content = b"shared content across users";
        let test_storage = TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage manager".to_string())?;

        let user_a = "user_a";
        let user_b = "user_b";

        let file_a = FileInfo::new_with_storage(
            create_test_file(content, "a.txt")?,
            &db,
            user_a,
            test_storage.storage(),
        )
        .await?;
        let file_b = FileInfo::new_with_storage(
            create_test_file(content, "b.txt")?,
            &db,
            user_b,
            test_storage.storage(),
        )
        .await?;

        assert_eq!(file_a.sha256, file_b.sha256);
        assert_ne!(file_a.id, file_b.id);
        assert_eq!(file_a.user_id, user_a);
        assert_eq!(file_b.user_id, user_b);
        assert_ne!(file_a.path, file_b.path);

        Ok(())
    }

    #[tokio::test]
    async fn test_get_by_sha_not_found_for_other_user() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let now = Utc::now();
        let sha = "abc123sha";
        let owner = "owner_user";
        let other = "other_user";

        db.store_item(FileInfo {
            id: Uuid::new_v4().to_string(),
            created_at: now,
            updated_at: now,
            user_id: owner.to_string(),
            sha256: sha.to_string(),
            path: format!("{owner}/id/file.txt"),
            file_name: "file.txt".to_string(),
            mime_type: "text/plain".to_string(),
        })
        .await?;

        let result = FileInfo::get_by_sha(sha, other, &db).await;
        assert!(matches!(result, Err(FileError::FileNotFound(_))));

        let found = FileInfo::get_by_sha(sha, owner, &db).await?;
        assert_eq!(found.sha256, sha);
        assert_eq!(found.user_id, owner);

        Ok(())
    }

    #[tokio::test]
    async fn test_new_with_storage_missing_file_name() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let test_storage = TestStorageManager::new_memory().await?;

        let field_data = create_test_file_without_name(b"data")?;
        let result =
            FileInfo::new_with_storage(field_data, &db, "user", test_storage.storage()).await;

        assert!(matches!(result, Err(FileError::MissingFileName)));

        Ok(())
    }

    #[tokio::test]
    async fn test_new_with_storage_empty_file() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let test_storage = TestStorageManager::new_memory().await?;

        let file_info = FileInfo::new_with_storage(
            create_test_file(&[], "empty.bin")?,
            &db,
            "empty_user",
            test_storage.storage(),
        )
        .await?;

        assert!(file_info.sha256.len() == 64);
        let bytes = file_info
            .get_content_with_storage(test_storage.storage())
            .await?;
        assert!(bytes.is_empty());

        Ok(())
    }

    #[tokio::test]
    async fn test_duplicate_upload_persists_single_row_per_user_sha() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        let test_storage = TestStorageManager::new_memory().await?;
        let storage = test_storage.storage();
        let user_id = "dedup_user";
        let content = b"dedup content";

        let first = FileInfo::new_with_storage(
            create_test_file(content, "first.txt")?,
            &db,
            user_id,
            storage,
        )
        .await?;
        let second = FileInfo::new_with_storage(
            create_test_file(content, "second.txt")?,
            &db,
            user_id,
            storage,
        )
        .await?;

        assert_eq!(first.id, second.id);
        assert_eq!(first.sha256, second.sha256);

        let rows: Vec<FileInfo> = db
            .client
            .query("SELECT * FROM file WHERE user_id = $user_id AND sha256 = $sha256")
            .bind(("user_id", user_id.to_string()))
            .bind(("sha256", first.sha256.clone()))
            .await?
            .take(0)?;
        assert_eq!(
            rows.len(),
            1,
            "unique (user_id, sha256) index should keep a single row"
        );

        Ok(())
    }

    #[tokio::test]
    async fn test_manual_file_info_creation() {
        let db = setup_test_db().await.expect("setup test db");
        // Create a FileInfo instance directly
        let now = Utc::now();
        let file_info = FileInfo {
            id: Uuid::new_v4().to_string(),
            created_at: now,
            updated_at: now,
            user_id: "user123".to_string(),
            sha256: "test_sha256_hash".to_string(),
            path: "/path/to/file.txt".to_string(),
            file_name: "manual_file.txt".to_string(),
            mime_type: "text/plain".to_string(),
        };

        // Store it in the database
        let result = db.store_item(file_info.clone()).await;
        assert!(result.is_ok());

        // Verify it can be retrieved
        let retrieved = db
            .get_item::<FileInfo>(&file_info.id)
            .await
            .expect("Failed to retrieve file info")
            .expect("expected file");
        assert_eq!(retrieved.id, file_info.id);
        assert_eq!(retrieved.sha256, file_info.sha256);
        assert_eq!(retrieved.file_name, file_info.file_name);
        assert_eq!(retrieved.path, file_info.path);
        assert_eq!(retrieved.mime_type, file_info.mime_type);
    }

    #[tokio::test]
    async fn test_delete_by_id() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        // Create and persist a test file via FileInfo::new_with_storage
        let user_id = "user123";
        let test_storage = TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage manager".to_string())?;
        let temp = create_test_file(b"test content", "test_file.txt")?;
        let file_info = FileInfo::new_with_storage(temp, &db, user_id, test_storage.storage())
            .await
            .with_context(|| "create file".to_string())?;

        // Delete the file using StorageManager
        let delete_result =
            FileInfo::delete_by_id_with_storage(&file_info.id, &db, test_storage.storage()).await;

        // Delete should be successful
        assert!(
            delete_result.is_ok(),
            "Failed to delete file: {delete_result:?}"
        );

        // Verify the file is removed from the database
        let retrieved: Option<FileInfo> = db
            .get_item(&file_info.id)
            .await
            .with_context(|| "Failed to query database".to_string())?;
        assert!(
            retrieved.is_none(),
            "FileInfo should be deleted from the database"
        );

        // Verify content no longer retrievable from storage
        assert!(test_storage.storage().get(&file_info.path).await.is_err());
        Ok(())
    }

    #[tokio::test]
    async fn test_delete_by_id_not_found() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        // Try to delete a file that doesn't exist
        let test_storage = TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage manager".to_string())?;
        let result =
            FileInfo::delete_by_id_with_storage("nonexistent_id", &db, test_storage.storage())
                .await;

        // Should succeed even if the file record does not exist
        assert!(result.is_ok());
        Ok(())
    }

    #[tokio::test]
    async fn test_get_by_id() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        // Create a FileInfo instance directly
        let now = Utc::now();
        let file_id = Uuid::new_v4().to_string();
        let original_file_info = FileInfo {
            id: file_id.clone(),
            user_id: "user123".to_string(),
            created_at: now,
            updated_at: now,
            sha256: "test_sha256_for_get_by_id".to_string(),
            path: "/path/to/get_by_id_test.txt".to_string(),
            file_name: "get_by_id_test.txt".to_string(),
            mime_type: "text/plain".to_string(),
        };

        // Store it in the database
        db.store_item(original_file_info.clone())
            .await
            .with_context(|| "Failed to store item for get_by_id test".to_string())?;

        // Retrieve it using get_by_id
        let retrieved_info = FileInfo::get_by_id(&file_id, &db)
            .await
            .with_context(|| "get_by_id".to_string())?;
        assert_eq!(retrieved_info.id, original_file_info.id);
        assert_eq!(retrieved_info.sha256, original_file_info.sha256);
        assert_eq!(retrieved_info.file_name, original_file_info.file_name);
        assert_eq!(retrieved_info.path, original_file_info.path);
        assert_eq!(retrieved_info.mime_type, original_file_info.mime_type);
        Ok(())
    }

    #[tokio::test]
    async fn test_get_by_id_not_found() -> anyhow::Result<()> {
        let db = setup_test_db().await?;
        // Try to retrieve a non-existent ID
        let non_existent_id = "non-existent-file-id";
        let result = FileInfo::get_by_id(non_existent_id, &db).await;

        // Assert failure
        assert!(result.is_err());

        // Assert the specific error type is FileNotFound
        match result {
            Err(FileError::FileNotFound(id)) => {
                assert_eq!(id, non_existent_id);
            }
            Err(e) => anyhow::bail!("Expected FileNotFound error, but got {e:?}"),
            Ok(_) => anyhow::bail!("Expected an error, but got Ok"),
        }
        Ok(())
    }

    // StorageManager-based tests
    #[tokio::test]
    async fn test_file_info_new_with_storage_memory() -> anyhow::Result<()> {
        // Setup
        let db = SurrealDbClient::memory("test_ns", "test_file_storage_memory")
            .await
            .with_context(|| "Failed to start DB".to_string())?;
        db.apply_migrations()
            .await
            .with_context(|| "migrations".to_string())?;

        let content = b"This is a test file for StorageManager";
        let field_data = create_test_file(content, "test_storage.txt")?;
        let user_id = "test_user";

        // Create test storage manager
        let storage = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage".to_string())?;

        // Test file creation with StorageManager
        let file_info = FileInfo::new_with_storage(field_data, &db, user_id, storage.storage())
            .await
            .with_context(|| "Failed to create file with StorageManager".to_string())?;

        // Verify the file was created correctly
        assert_eq!(file_info.user_id, user_id);
        assert_eq!(file_info.file_name, "test_storage.txt");
        assert!(!file_info.sha256.is_empty());
        assert!(!file_info.path.is_empty());

        // Test content retrieval with StorageManager
        let retrieved_content = file_info
            .get_content_with_storage(storage.storage())
            .await
            .with_context(|| "Failed to get file content with StorageManager".to_string())?;
        assert_eq!(retrieved_content.as_ref(), content);

        // Test file deletion with StorageManager
        FileInfo::delete_by_id_with_storage(&file_info.id, &db, storage.storage())
            .await
            .with_context(|| "Failed to delete file with StorageManager".to_string())?;

        // Verify file is deleted
        let deleted_content_result = file_info.get_content_with_storage(storage.storage()).await;
        assert!(deleted_content_result.is_err());
        Ok(())
    }

    #[tokio::test]
    async fn test_file_info_new_with_storage_local() -> anyhow::Result<()> {
        // Setup
        let db = SurrealDbClient::memory("test_ns", "test_file_storage_local")
            .await
            .with_context(|| "Failed to start DB".to_string())?;
        db.apply_migrations()
            .await
            .with_context(|| "migrations".to_string())?;

        let content = b"This is a test file for StorageManager with local storage";
        let field_data = create_test_file(content, "test_local.txt")?;
        let user_id = "test_user";

        // Create test storage manager with local backend
        let storage = store::testing::TestStorageManager::new_local()
            .await
            .with_context(|| "create test storage".to_string())?;

        // Test file creation with StorageManager
        let file_info = FileInfo::new_with_storage(field_data, &db, user_id, storage.storage())
            .await
            .with_context(|| "Failed to create file with StorageManager".to_string())?;

        // Verify the file was created correctly
        assert_eq!(file_info.user_id, user_id);
        assert_eq!(file_info.file_name, "test_local.txt");
        assert!(!file_info.sha256.is_empty());
        assert!(!file_info.path.is_empty());

        // Test content retrieval with StorageManager
        let retrieved_content = file_info
            .get_content_with_storage(storage.storage())
            .await
            .with_context(|| "Failed to get file content with StorageManager".to_string())?;
        assert_eq!(retrieved_content.as_ref(), content);

        // Test file deletion with StorageManager
        FileInfo::delete_by_id_with_storage(&file_info.id, &db, storage.storage())
            .await
            .with_context(|| "Failed to delete file with StorageManager".to_string())?;

        // Verify file is deleted
        let deleted_content_result = file_info.get_content_with_storage(storage.storage()).await;
        assert!(deleted_content_result.is_err());
        Ok(())
    }

    #[tokio::test]
    async fn test_file_info_storage_manager_persistence() -> anyhow::Result<()> {
        // Setup
        let db = SurrealDbClient::memory("test_ns", "test_file_persistence")
            .await
            .with_context(|| "Failed to start DB".to_string())?;
        db.apply_migrations()
            .await
            .with_context(|| "migrations".to_string())?;

        let content = b"Test content for persistence";
        let field_data = create_test_file(content, "persistence_test.txt")?;
        let user_id = "test_user";

        // Create test storage manager
        let storage = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "create test storage".to_string())?;

        // Create file
        let file_info = FileInfo::new_with_storage(field_data, &db, user_id, storage.storage())
            .await
            .with_context(|| "Failed to create file".to_string())?;

        // Test that data persists across multiple operations with the same StorageManager
        let retrieved_content_1 = file_info
            .get_content_with_storage(storage.storage())
            .await
            .with_context(|| "get content 1".to_string())?;
        let retrieved_content_2 = file_info
            .get_content_with_storage(storage.storage())
            .await
            .with_context(|| "get content 2".to_string())?;

        assert_eq!(retrieved_content_1.as_ref(), content);
        assert_eq!(retrieved_content_2.as_ref(), content);

        // Test that different StorageManager instances don't share data (memory storage isolation)
        let storage2 = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "create second storage".to_string())?;
        let isolated_content_result = file_info.get_content_with_storage(storage2.storage()).await;
        assert!(
            isolated_content_result.is_err(),
            "Different StorageManager should not have access to same data"
        );
        Ok(())
    }

    #[tokio::test]
    async fn test_file_info_storage_manager_equivalence() -> anyhow::Result<()> {
        // Setup
        let db = SurrealDbClient::memory("test_ns", "test_file_equivalence")
            .await
            .with_context(|| "Failed to start DB".to_string())?;
        db.apply_migrations()
            .await
            .with_context(|| "migrations".to_string())?;

        let content = b"Test content for equivalence testing";
        let field_data1 = create_test_file(content, "equivalence_test_1.txt")?;
        let field_data2 = create_test_file(content, "equivalence_test_2.txt")?;
        let user_id = "test_user";

        // Create single storage manager and reuse it
        let storage_manager = store::testing::TestStorageManager::new_memory()
            .await
            .with_context(|| "create storage".to_string())?;
        let storage = storage_manager.storage();

        // Create multiple files with the same storage manager
        let file_info_1 = FileInfo::new_with_storage(field_data1, &db, user_id, storage)
            .await
            .with_context(|| "Failed to create file 1".to_string())?;

        let file_info_2 = FileInfo::new_with_storage(field_data2, &db, user_id, storage)
            .await
            .with_context(|| "Failed to create file 2".to_string())?;

        // Test that both files can be retrieved with the same storage backend
        let content_1 = file_info_1
            .get_content_with_storage(storage)
            .await
            .with_context(|| "get file 1 content".to_string())?;
        let content_2 = file_info_2
            .get_content_with_storage(storage)
            .await
            .with_context(|| "get file 2 content".to_string())?;

        assert_eq!(content_1.as_ref(), content);
        assert_eq!(content_2.as_ref(), content);

        // Test that files can be deleted with the same storage manager
        FileInfo::delete_by_id_with_storage(&file_info_1.id, &db, storage)
            .await
            .with_context(|| "delete file 1".to_string())?;
        FileInfo::delete_by_id_with_storage(&file_info_2.id, &db, storage)
            .await
            .with_context(|| "delete file 2".to_string())?;

        // Verify files are deleted
        let deleted_content_1 = file_info_1.get_content_with_storage(storage).await;
        let deleted_content_2 = file_info_2.get_content_with_storage(storage).await;

        assert!(deleted_content_1.is_err());
        assert!(deleted_content_2.is_err());
        Ok(())
    }
}
