//! HCFS Client Module
//!
//! HTTP client for communicating with the HCFS Server.
//! Handles all network operations: upload, download, delete, and state synchronization.

use crate::sync::{SyncError, SyncResult};
use bytes::Bytes;
use futures_util::StreamExt;
use hcfs_shared::network::{
    DeleteResponse, DeleteResult, DownloadResult, GetStateResponse, GetStateResult,
    ListFoldersResponse, Manifest, NetworkResponse, RegisterFolderRequest, RegisterFolderResponse,
    RemoteFileEntry, RemoteFolderInfo, UnregisterFolderRequest, UnregisterFolderResponse,
    UnregisterFolderResult, UploadResponse, UploadResult,
};
use reqwest::multipart::{Form, Part};
use reqwest::{Body, Client};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc;
use tracing::{info, warn};

/// Default chunk size for streaming HTTP uploads (8MB)
///
/// AWS S3 recommends 16-64MB for multipart uploads. 8MB provides a good balance
/// between memory usage and network efficiency. This is 125x larger than the old
/// 64KB chunks, reducing syscalls and HTTP body writes significantly.
const UPLOAD_CHUNK_SIZE: usize = 8 * 1024 * 1024;

// =============================================================================
// Header Parsing Helpers
// =============================================================================

/// Parse a numeric header value, logging a warning if missing or invalid
fn parse_header_u64(headers: &reqwest::header::HeaderMap, name: &str, file_id: &str) -> u64 {
    match headers.get(name) {
        Some(v) => v
            .to_str()
            .ok()
            .and_then(|s| s.parse().ok())
            .unwrap_or_else(|| {
                warn!(file_id = %file_id, header = %name, "Invalid header value");
                0
            }),
        None => {
            warn!(file_id = %file_id, header = %name, "Missing header");
            0
        }
    }
}

/// Parse a 32-byte hex revision ID from headers, logging warnings for issues
fn parse_revision_id_header(headers: &reqwest::header::HeaderMap, file_id: &str) -> [u8; 32] {
    let mut revision_id = [0u8; 32];

    let Some(header_value) = headers.get("X-Revision-Id") else {
        warn!(file_id = %file_id, "Missing X-Revision-Id header");
        return revision_id;
    };

    let Ok(hex_str) = header_value.to_str() else {
        warn!(file_id = %file_id, "X-Revision-Id is not valid UTF-8");
        return revision_id;
    };

    if hex_str.len() != 64 {
        warn!(file_id = %file_id, len = hex_str.len(), "X-Revision-Id has wrong hex length");
        return revision_id;
    }

    // Decode directly into fixed array — zero allocation
    if hex::decode_to_slice(hex_str, &mut revision_id).is_err() {
        warn!(file_id = %file_id, "X-Revision-Id is not valid hex");
        revision_id = [0u8; 32];
    }

    revision_id
}

/// Metadata extracted from download response headers
struct DownloadMetadata {
    revision_seq: u64,
    revision_id: [u8; 32],
    size_bytes: u64,
}

/// Extract all download metadata from response headers
fn extract_download_metadata(
    headers: &reqwest::header::HeaderMap,
    file_id: &str,
) -> DownloadMetadata {
    DownloadMetadata {
        revision_seq: parse_header_u64(headers, "X-Revision-Seq", file_id),
        revision_id: parse_revision_id_header(headers, file_id),
        size_bytes: parse_header_u64(headers, "X-Size-Bytes", file_id),
    }
}

/// Handle upload response - parse success, conflict, or error.
/// Reads the body as text first so middleware errors (401, 429) that
/// don't use the `NetworkResponse` envelope still produce useful messages.
async fn handle_upload_response(
    response: reqwest::Response,
    path_hash: [u8; 32],
) -> SyncResult<UploadResult> {
    let status = response.status();
    let body = response.text().await.unwrap_or_default();

    let upload_response: UploadResponse = serde_json::from_str(&body)
        .map_err(|_| SyncError::UploadFailed(format!("Server returned {}: {}", status, body,)))?;

    match upload_response {
        NetworkResponse::Success(upload_result) => {
            info!(
                upload_id = %upload_result.upload_id,
                "File uploaded successfully"
            );
            Ok(upload_result)
        }

        NetworkResponse::Conflict(conflict) => Err(SyncError::Conflict {
            file_id: path_hash,
            current_revision_id: conflict.current_revision_id,
            message: conflict.message,
        }),

        NetworkResponse::Error(error_response) => Err(SyncError::UploadFailed(format!(
            "Server returned {}: {}",
            status,
            serde_json::to_string(&error_response).unwrap_or(body),
        ))),
    }
}

/// Stream response bytes to file, optionally reporting progress.
/// Returns total bytes written on success, or cleans up partial file on error.
async fn stream_to_file<F>(
    response: reqwest::Response,
    output_path: &Path,
    file_id: &str,
    progress: Option<F>,
) -> SyncResult<u64>
where
    F: Fn(u64, u64),
{
    let total_size = response.content_length().unwrap_or(0);

    // Signal download starting
    if let Some(ref p) = progress {
        p(0, total_size);
    }

    let mut file = tokio::fs::File::create(output_path)
        .await
        .map_err(|e| SyncError::DownloadFailed(format!("Failed to create output file: {}", e)))?;

    let mut stream = response.bytes_stream();
    let mut bytes_written = 0u64;

    let result = async {
        while let Some(chunk_result) = stream.next().await {
            let chunk = chunk_result
                .map_err(|e| SyncError::DownloadFailed(format!("Failed to read chunk: {}", e)))?;

            tokio::io::AsyncWriteExt::write_all(&mut file, &chunk)
                .await
                .map_err(|e| SyncError::DownloadFailed(format!("Failed to write chunk: {}", e)))?;

            bytes_written += chunk.len() as u64;

            if let Some(ref p) = progress {
                p(bytes_written, total_size);
            }
        }

        tokio::io::AsyncWriteExt::flush(&mut file)
            .await
            .map_err(|e| SyncError::DownloadFailed(format!("Failed to flush file: {}", e)))?;

        Ok::<_, SyncError>(bytes_written)
    }
    .await;

    // Clean up partial file on error
    if let Err(ref e) = result {
        warn!(
            file_id = %file_id,
            output_path = %output_path.display(),
            error = %e,
            "Download failed, cleaning up partial file"
        );
        let _ = tokio::fs::remove_file(output_path).await;
    }

    result
}

// =============================================================================
// HCFS Client Configuration
// =============================================================================

/// Client configuration
#[derive(Clone, Debug, Deserialize, Serialize)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct HcfsClientConfig {
    /// Base URL of the server (e.g., "https://localhost:9999")
    pub base_url: String,
    /// API key for authentication
    pub api_key: String,
    /// Bearer token for authorization
    pub bearer_token: String,
    /// Whether to accept invalid TLS certificates (for testing)
    pub accept_invalid_certs: bool,
    /// Optional billing bypass token (skips billing validation on the server)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub billing_bypass_token: Option<String>,
    /// SS58 address of the user's Substrate/Bittensor account
    #[serde(default, alias = "account_ss58")]
    pub ss58_address: String,
    /// 16-char hex folder hash
    #[serde(default)]
    pub folder_hash: String,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl HcfsClientConfig {
    #[new]
    #[pyo3(signature = (base_url, api_key, bearer_token, ss58_address, folder_hash="".to_string(), accept_invalid_certs=false))]
    fn py_new(
        base_url: String,
        api_key: String,
        bearer_token: String,
        ss58_address: String,
        folder_hash: String,
        accept_invalid_certs: bool,
    ) -> Self {
        Self {
            base_url,
            api_key,
            bearer_token,
            accept_invalid_certs,
            billing_bypass_token: None,
            ss58_address,
            folder_hash,
        }
    }

    #[getter]
    fn base_url(&self) -> &str {
        &self.base_url
    }

    #[setter]
    fn set_base_url(&mut self, value: String) {
        self.base_url = value;
    }

    #[getter]
    fn api_key(&self) -> &str {
        &self.api_key
    }

    #[setter]
    fn set_api_key(&mut self, value: String) {
        self.api_key = value;
    }

    #[getter]
    fn bearer_token(&self) -> &str {
        &self.bearer_token
    }

    #[setter]
    fn set_bearer_token(&mut self, value: String) {
        self.bearer_token = value;
    }

    #[getter]
    fn accept_invalid_certs(&self) -> bool {
        self.accept_invalid_certs
    }

    #[setter]
    fn set_accept_invalid_certs(&mut self, value: bool) {
        self.accept_invalid_certs = value;
    }

    #[getter]
    fn ss58_address(&self) -> &str {
        &self.ss58_address
    }

    #[setter]
    fn set_ss58_address(&mut self, value: String) {
        self.ss58_address = value;
    }

    #[getter]
    fn folder_hash(&self) -> &str {
        &self.folder_hash
    }

    #[setter]
    fn set_folder_hash(&mut self, value: String) {
        self.folder_hash = value;
    }

    #[pyo3(name = "save")]
    fn py_save(&self, drive_path: &str) -> pyo3::PyResult<()> {
        self.save(Path::new(drive_path))
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
    }

    #[staticmethod]
    #[pyo3(name = "load")]
    fn py_load(drive_path: &str) -> Option<Self> {
        Self::load(Path::new(drive_path))
    }
}

impl Default for HcfsClientConfig {
    fn default() -> Self {
        Self {
            base_url: "https://localhost:9999".to_string(),
            api_key: "test-api-key".to_string(),
            bearer_token: "test-bearer-token".to_string(),
            accept_invalid_certs: true, // For development/testing
            billing_bypass_token: None,
            ss58_address: String::new(),
            folder_hash: String::new(),
        }
    }
}

impl HcfsClientConfig {
    #[inline(always)]
    pub fn get_path(drive_path: &Path) -> PathBuf {
        drive_path.join(".hippius").join("server.json")
    }

    pub fn load(drive_path: &Path) -> Option<HcfsClientConfig> {
        let path = Self::get_path(drive_path);
        if path.exists() {
            let contents = std::fs::read_to_string(&path).ok()?;
            serde_json::from_str(&contents).ok()
        } else {
            None
        }
    }

    pub fn save(&self, drive_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
        let path = Self::get_path(drive_path);
        let contents = serde_json::to_string_pretty(self)?;
        std::fs::write(&path, contents)?;
        Ok(())
    }
}

// =============================================================================
// HCFS Client
// =============================================================================

/// HTTP client for server communication
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
pub struct HcfsClient {
    config: HcfsClientConfig,
    client: Client,
    /// Override bearer token (e.g., ss58_address from Drive after unlock)
    bearer_token_override: Option<String>,
    /// Pre-built headers, rebuilt only when bearer token changes
    cached_headers: reqwest::header::HeaderMap,
}

#[cfg(feature = "python")]
#[pyo3::prelude::pymethods]
impl HcfsClient {
    #[new]
    fn py_new(config: HcfsClientConfig) -> pyo3::PyResult<Self> {
        Self::new(config).map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
    }

    #[pyo3(name = "download")]
    #[pyo3(signature = (ss58_address, folder_hash, file_id, output_path, progress=None))]
    fn py_download(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        file_id: &str,
        output_path: &str,
        progress: Option<pyo3::PyObject>,
    ) -> pyo3::PyResult<hcfs_shared::network::DownloadResult> {
        let rt = tokio::runtime::Runtime::new()
            .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))?;

        match progress {
            Some(cb) => {
                let progress_fn = move |received: u64, total: u64| {
                    pyo3::Python::with_gil(|py| {
                        let _ = cb.call1(py, (received, total));
                    });
                };
                rt.block_on(self.download(
                    ss58_address,
                    folder_hash,
                    file_id,
                    std::path::Path::new(output_path),
                    Some(progress_fn),
                ))
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
            }
            None => rt
                .block_on(self.download(
                    ss58_address,
                    folder_hash,
                    file_id,
                    std::path::Path::new(output_path),
                    None::<fn(u64, u64)>,
                ))
                .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string())),
        }
    }
}

impl HcfsClient {
    /// Create a new server client
    pub fn new(config: HcfsClientConfig) -> SyncResult<Self> {
        let client = Client::builder()
            .danger_accept_invalid_certs(config.accept_invalid_certs)
            .read_timeout(std::time::Duration::from_secs(600)) // 10 min for server processing
            .connect_timeout(std::time::Duration::from_secs(30))
            .tcp_keepalive(std::time::Duration::from_secs(30)) // Keep connection alive during long uploads
            .pool_idle_timeout(std::time::Duration::from_secs(600)) // Don't drop idle connections quickly
            .build()
            .map_err(|e| SyncError::Network(e.to_string()))?;

        if config.accept_invalid_certs {
            warn!("TLS certificate validation is disabled - only use this for development/testing");
        }

        let cached_headers = Self::build_headers(
            &config.api_key,
            &config.bearer_token,
            config.billing_bypass_token.as_deref(),
        );

        Ok(Self {
            config,
            client,
            bearer_token_override: None,
            cached_headers,
        })
    }

    /// The bearer token from the original config (before any override).
    pub fn configured_bearer_token(&self) -> &str {
        &self.config.bearer_token
    }

    /// Set the bearer token override (used to auto-use ss58_address after unlock)
    pub fn set_bearer_token(&mut self, token: String) {
        self.cached_headers = Self::build_headers(
            &self.config.api_key,
            &token,
            self.config.billing_bypass_token.as_deref(),
        );
        self.bearer_token_override = Some(token);
    }

    /// Build headers from API key, bearer token, and optional billing bypass
    fn build_headers(
        api_key: &str,
        bearer_token: &str,
        billing_bypass_token: Option<&str>,
    ) -> reqwest::header::HeaderMap {
        let capacity = 2 + usize::from(billing_bypass_token.is_some());
        let mut headers = reqwest::header::HeaderMap::with_capacity(capacity);
        if let Ok(api_key) = api_key.parse() {
            headers.insert("X-API-Key", api_key);
        }
        if let Ok(auth) = format!("Bearer {}", bearer_token).parse() {
            headers.insert(reqwest::header::AUTHORIZATION, auth);
        }
        if let Some(token) = billing_bypass_token
            && let Ok(val) = token.parse()
        {
            headers.insert("X-Billing-Bypass", val);
        }
        headers
    }

    /// Get default headers for requests (cheap clone of 2-entry cached map)
    fn default_headers(&self) -> reqwest::header::HeaderMap {
        self.cached_headers.clone()
    }

    // =========================================================================
    // Get State (with pagination)
    // =========================================================================

    /// Fetch remote state for a user with pagination
    pub async fn get_state(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        offset: u32,
        limit: u32,
    ) -> SyncResult<GetStateResult> {
        let encoded_ss58 = urlencoding::encode(ss58_address);
        let encoded_fhash = urlencoding::encode(folder_hash);
        let url = format!(
            "{}/get_state/{}/{}?offset={}&limit={}",
            self.config.base_url, encoded_ss58, encoded_fhash, offset, limit
        );

        info!(
            ss58_address = %ss58_address,
            folder_hash = %folder_hash,
            offset = offset,
            limit = limit,
            "Requesting remote state from server"
        );

        let response = self
            .client
            .get(&url)
            .headers(self.default_headers())
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();
        let body = response.text().await.unwrap_or_default();

        let state: GetStateResponse = serde_json::from_str(&body).map_err(|_| {
            SyncError::RemoteFetch(format!("Server returned {}: {}", status, body,))
        })?;

        match state {
            NetworkResponse::Success(state) => {
                info!(
                    ss58_address = %ss58_address,
                    total_files = state.total_count,
                    page_files = state.files.len(),
                    has_more = state.has_more,
                    "Fetched remote state"
                );
                Ok(state)
            }

            NetworkResponse::Conflict(_) => Err(SyncError::RemoteFetch(
                "Unexpected conflict response from get_state endpoint".into(),
            )),

            NetworkResponse::Error(error_response) => Err(SyncError::RemoteFetch(format!(
                "Server returned {}: {}",
                status,
                serde_json::to_string(&error_response).unwrap_or(body),
            ))),
        }
    }

    /// Fetch all files for a user (handles pagination internally)
    ///
    /// # Arguments
    /// * `progress` - Optional callback receiving (files_fetched, total_estimated)
    pub async fn get_all_files<F>(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        progress: Option<F>,
    ) -> SyncResult<Vec<RemoteFileEntry>>
    where
        F: Fn(u64, u64),
    {
        let mut all_files = Vec::new();
        let mut offset = 0u32;
        let limit = 1000u32;

        // Report initial progress
        if let Some(ref p) = progress {
            p(0, 0);
        }

        loop {
            let response = self
                .get_state(ss58_address, folder_hash, offset, limit)
                .await?;

            // Get total count from server response
            let total_count = response.total_count as u64;

            all_files.extend(response.files);

            // Report progress
            if let Some(ref p) = progress {
                p(all_files.len() as u64, total_count);
            }

            if !response.has_more {
                break;
            }

            offset += limit;
        }

        Ok(all_files)
    }

    // =========================================================================
    // Upload
    // =========================================================================

    /// Upload an encrypted file to the server (streaming from file)
    ///
    /// # Arguments
    /// * `progress` - Optional callback receiving (bytes_sent, total_bytes)
    pub async fn upload<F>(
        &self,
        manifest: Manifest,
        ciphertext_path: &Path,
        progress: Option<F>,
    ) -> SyncResult<UploadResult>
    where
        F: Fn(u64, u64) + Send + Sync + 'static,
    {
        let url = format!("{}/upload", self.config.base_url);

        // Get file size for content length
        let file_metadata = std::fs::metadata(ciphertext_path).map_err(|e| {
            SyncError::UploadFailed(format!("Failed to read ciphertext file: {}", e))
        })?;
        let total_size = file_metadata.len();

        info!(
            path_hash = %hex::encode(&manifest.path_hash[..8]),
            ss58_address = %manifest.ss58_address,
            size_bytes = total_size,
            revision_seq = manifest.revision_seq,
            has_base_revision = manifest.base_revision_id.is_some(),
            "Uploading encrypted file to server (streaming)"
        );

        // Signal upload starting (0, total)
        if let Some(ref p) = progress {
            p(0, total_size);
        }

        // Create multipart form
        let manifest_json = serde_json::to_vec(&manifest)
            .map_err(|e| SyncError::UploadFailed(format!("Failed to serialize manifest: {}", e)))?;

        // Create a streaming body with progress tracking from file
        let bytes_sent = Arc::new(AtomicU64::new(0));
        let bytes_sent_clone = bytes_sent.clone();
        let progress = progress.map(Arc::new);
        let progress_clone = progress.clone();

        // Open file for streaming
        let mut file = tokio::fs::File::open(ciphertext_path).await.map_err(|e| {
            SyncError::UploadFailed(format!("Failed to open ciphertext file: {}", e))
        })?;

        // Create an async stream that reads chunks from file and reports progress
        // Uses Bytes for zero-copy and batches progress updates for efficiency
        let stream = async_stream::stream! {
            let mut buffer = vec![0u8; UPLOAD_CHUNK_SIZE];
            let mut last_progress_bytes = 0u64;
            const PROGRESS_UPDATE_INTERVAL: u64 = 1024 * 1024; // Update progress every 1MB

            loop {
                match file.read(&mut buffer).await {
                    Ok(0) => {
                        // EOF - send final progress update
                        let sent = bytes_sent_clone.load(Ordering::Relaxed);
                        if let Some(ref p) = progress_clone {
                            p(sent, total_size);
                        }
                        break;
                    }
                    Ok(n) => {
                        // Use Bytes::copy_from_slice for efficient buffer management
                        let chunk = Bytes::copy_from_slice(&buffer[..n]);
                        let chunk_len = chunk.len() as u64;

                        let sent = bytes_sent_clone.fetch_add(chunk_len, Ordering::Relaxed) + chunk_len;

                        // Batch progress updates - only report every 1MB or on large chunks
                        if sent - last_progress_bytes >= PROGRESS_UPDATE_INTERVAL || chunk_len >= PROGRESS_UPDATE_INTERVAL {
                            if let Some(ref p) = progress_clone {
                                p(sent, total_size);
                            }
                            last_progress_bytes = sent;
                        }

                        yield Ok::<_, std::io::Error>(chunk);
                    }
                    Err(e) => {
                        yield Err(e);
                        break;
                    }
                }
            }
        };

        let body = Body::wrap_stream(stream);
        let part = Part::stream_with_length(body, total_size)
            .mime_str("application/octet-stream")
            .unwrap();

        let form = Form::new()
            .part(
                "manifest",
                Part::bytes(manifest_json)
                    .mime_str("application/json")
                    .unwrap(),
            )
            .part("ciphertext", part);

        let response = self
            .client
            .post(&url)
            .headers(self.default_headers())
            .multipart(form)
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        // Signal upload complete
        if let Some(ref p) = progress {
            p(total_size, total_size);
        }

        handle_upload_response(response, manifest.path_hash).await
    }

    /// Upload an encrypted file from an in-memory channel (streaming without temp file)
    ///
    /// This is the high-performance upload path that pipelines encryption with network I/O.
    /// Encrypted chunks are received from the channel and streamed directly to the server.
    ///
    /// # Arguments
    /// * `manifest` - File manifest with metadata
    /// * `rx` - Channel receiver providing encrypted data chunks as Bytes
    /// * `total_size` - Total ciphertext size for Content-Length header
    /// * `progress` - Optional callback receiving (bytes_sent, total_bytes)
    pub async fn upload_from_channel<F>(
        &self,
        manifest: Manifest,
        mut rx: mpsc::Receiver<Bytes>,
        total_size: u64,
        progress: Option<F>,
    ) -> SyncResult<UploadResult>
    where
        F: Fn(u64, u64) + Send + Sync + 'static,
    {
        let url = format!("{}/upload", self.config.base_url);

        info!(
            path_hash = %hex::encode(&manifest.path_hash[..8]),
            ss58_address = %manifest.ss58_address,
            size_bytes = total_size,
            revision_seq = manifest.revision_seq,
            has_base_revision = manifest.base_revision_id.is_some(),
            "Uploading encrypted file to server (streaming from channel)"
        );

        // Signal upload starting (0, total)
        if let Some(ref p) = progress {
            p(0, total_size);
        }

        // Create multipart form
        let manifest_json = serde_json::to_vec(&manifest)
            .map_err(|e| SyncError::UploadFailed(format!("Failed to serialize manifest: {}", e)))?;

        // Track bytes sent for progress reporting
        let bytes_sent = Arc::new(AtomicU64::new(0));
        let bytes_sent_clone = bytes_sent.clone();
        let progress = progress.map(Arc::new);
        let progress_clone = progress.clone();

        // Create an async stream from channel receiver with batched progress updates
        let stream = async_stream::stream! {
            let mut last_progress_bytes = 0u64;
            const PROGRESS_UPDATE_INTERVAL: u64 = 1024 * 1024; // Update progress every 1MB

            while let Some(chunk) = rx.recv().await {
                let chunk_len = chunk.len() as u64;
                let sent = bytes_sent_clone.fetch_add(chunk_len, Ordering::Relaxed) + chunk_len;

                // Batch progress updates - only report every 1MB or on large chunks
                if sent - last_progress_bytes >= PROGRESS_UPDATE_INTERVAL || chunk_len >= PROGRESS_UPDATE_INTERVAL {
                    if let Some(ref p) = progress_clone {
                        p(sent, total_size);
                    }
                    last_progress_bytes = sent;
                }

                yield Ok::<_, std::io::Error>(chunk);
            }

            // Send final progress update
            let sent = bytes_sent_clone.load(Ordering::Relaxed);
            if let Some(ref p) = progress_clone {
                p(sent, total_size);
            }
        };

        let body = Body::wrap_stream(stream);
        let part = Part::stream_with_length(body, total_size)
            .mime_str("application/octet-stream")
            .unwrap();

        let form = Form::new()
            .part(
                "manifest",
                Part::bytes(manifest_json)
                    .mime_str("application/json")
                    .unwrap(),
            )
            .part("ciphertext", part);

        let response = self
            .client
            .post(&url)
            .headers(self.default_headers())
            .multipart(form)
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        // Signal upload complete
        if let Some(ref p) = progress {
            p(total_size, total_size);
        }

        handle_upload_response(response, manifest.path_hash).await
    }

    // =========================================================================
    // Download
    // =========================================================================

    /// Download an encrypted file from the server (streaming to file)
    ///
    /// # Arguments
    /// * `progress` - Optional callback receiving (bytes_received, total_bytes)
    pub async fn download<F>(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        file_id: &str,
        output_path: &Path,
        progress: Option<F>,
    ) -> SyncResult<DownloadResult>
    where
        F: Fn(u64, u64),
    {
        let encoded_ss58 = urlencoding::encode(ss58_address);
        let encoded_fhash = urlencoding::encode(folder_hash);
        let encoded_file_id = urlencoding::encode(file_id);
        let url = format!(
            "{}/download/{}/{}/{}",
            self.config.base_url, encoded_ss58, encoded_fhash, encoded_file_id
        );

        info!(
            ss58_address = %ss58_address,
            file_id = %file_id,
            output_path = %output_path.display(),
            "Downloading encrypted file from server"
        );

        let response = self
            .client
            .get(&url)
            .headers(self.default_headers())
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();

        if status.is_success() {
            let metadata = extract_download_metadata(response.headers(), file_id);
            let bytes_written = stream_to_file(response, output_path, file_id, progress).await?;

            info!(file_id = %file_id, size = bytes_written, "File downloaded successfully");

            Ok(DownloadResult {
                revision_seq: metadata.revision_seq,
                revision_id: metadata.revision_id,
                size_bytes: metadata.size_bytes,
            })
        } else if status == reqwest::StatusCode::NOT_FOUND {
            Err(SyncError::FileNotFound(file_id.to_string()))
        } else {
            let error_text = response.text().await.unwrap_or_default();
            Err(SyncError::DownloadFailed(format!(
                "Server returned {}: {}",
                status, error_text
            )))
        }
    }

    // =========================================================================
    // Delete
    // =========================================================================

    /// Delete a file from the server
    pub async fn delete(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        file_id: &str,
    ) -> SyncResult<DeleteResult> {
        let encoded_ss58 = urlencoding::encode(ss58_address);
        let encoded_fhash = urlencoding::encode(folder_hash);
        let encoded_file_id = urlencoding::encode(file_id);
        let url = format!(
            "{}/delete/{}/{}/{}",
            self.config.base_url, encoded_ss58, encoded_fhash, encoded_file_id
        );

        info!(
            ss58_address = %ss58_address,
            file_id = %file_id,
            "Requesting file deletion from server"
        );

        let response = self
            .client
            .delete(&url)
            .headers(self.default_headers())
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();
        let body = response.text().await.unwrap_or_default();

        let delete_response: DeleteResponse = serde_json::from_str(&body).map_err(|_| {
            SyncError::RemoteDelete(format!("Server returned {}: {}", status, body,))
        })?;

        match delete_response {
            NetworkResponse::Success(delete_result) => {
                info!(file_id = %file_id, "File deleted successfully");
                Ok(delete_result)
            }

            NetworkResponse::Conflict(_) => Err(SyncError::RemoteDelete(
                "Unexpected conflict response from delete endpoint".into(),
            )),

            NetworkResponse::Error(error) => Err(SyncError::RemoteDelete(format!(
                "Server returned {}: {}",
                status,
                serde_json::to_string(&error).unwrap_or(body),
            ))),
        }
    }

    // =========================================================================
    // Folder Registry
    // =========================================================================

    /// Register a folder label with the server for cross-device discovery.
    pub async fn register_folder(
        &self,
        ss58_address: &str,
        folder_hash: &str,
        label: &str,
        device_name: Option<&str>,
    ) -> SyncResult<()> {
        let url = format!("{}/register_folder", self.config.base_url);

        let body = RegisterFolderRequest {
            ss58_address: ss58_address.to_string(),
            folder_hash: folder_hash.to_string(),
            label: label.to_string(),
            device_name: device_name.map(|s| s.to_string()),
        };

        let response = self
            .client
            .post(&url)
            .headers(self.default_headers())
            .json(&body)
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();
        let text = response.text().await.unwrap_or_default();

        let parsed: RegisterFolderResponse = serde_json::from_str(&text).map_err(|_| {
            SyncError::Network(format!(
                "Failed to parse register_folder response ({}): {}",
                status, text
            ))
        })?;

        match parsed {
            NetworkResponse::Success(_) => Ok(()),
            NetworkResponse::Conflict(_) => Err(SyncError::Network(
                "Unexpected conflict from register_folder".into(),
            )),
            NetworkResponse::Error(e) => Err(SyncError::Network(format!(
                "register_folder failed: {}",
                e.message
            ))),
        }
    }

    /// List all folders registered for a base address.
    pub async fn list_remote_folders(
        &self,
        base_address: &str,
    ) -> SyncResult<Vec<RemoteFolderInfo>> {
        let url = format!("{}/list_folders/{}", self.config.base_url, base_address);

        let response = self
            .client
            .get(&url)
            .headers(self.default_headers())
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();
        let text = response.text().await.unwrap_or_default();

        let parsed: ListFoldersResponse = serde_json::from_str(&text).map_err(|_| {
            SyncError::Network(format!(
                "Failed to parse list_folders response ({}): {}",
                status, text
            ))
        })?;

        match parsed {
            NetworkResponse::Success(result) => Ok(result.folders),
            NetworkResponse::Conflict(_) => Err(SyncError::Network(
                "Unexpected conflict from list_folders".into(),
            )),
            NetworkResponse::Error(e) => Err(SyncError::Network(format!(
                "list_folders failed: {}",
                e.message
            ))),
        }
    }

    // =========================================================================
    // Unregister Folder
    // =========================================================================

    /// Delete all files for a folder and unregister it from the server.
    pub async fn unregister_folder(
        &self,
        ss58_address: &str,
        folder_hash: &str,
    ) -> SyncResult<UnregisterFolderResult> {
        let url = format!("{}/unregister_folder", self.config.base_url);

        let body = UnregisterFolderRequest {
            ss58_address: ss58_address.to_string(),
            folder_hash: folder_hash.to_string(),
        };

        let response = self
            .client
            .delete(&url)
            .headers(self.default_headers())
            .json(&body)
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        let status = response.status();
        let text = response.text().await.unwrap_or_default();

        let parsed: UnregisterFolderResponse = serde_json::from_str(&text).map_err(|_| {
            SyncError::Network(format!(
                "Failed to parse unregister_folder response ({}): {}",
                status, text
            ))
        })?;

        match parsed {
            NetworkResponse::Success(result) => Ok(result),
            NetworkResponse::Conflict(_) => Err(SyncError::Network(
                "Unexpected conflict from unregister_folder".into(),
            )),
            NetworkResponse::Error(e) => Err(SyncError::Network(format!(
                "unregister_folder failed: {}",
                e.message
            ))),
        }
    }

    // =========================================================================
    // Health Check
    // =========================================================================

    /// Check if the server is reachable
    pub async fn health_check(&self) -> SyncResult<bool> {
        let url = format!("{}/health", self.config.base_url);

        let response = self
            .client
            .get(&url)
            .headers(self.default_headers())
            .send()
            .await
            .map_err(|e| SyncError::Network(e.to_string()))?;

        Ok(response.status().is_success())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::io::Write;
    use wiremock::matchers::{method, path, path_regex, query_param};
    use wiremock::{Mock, MockServer, ResponseTemplate};

    #[test]
    fn test_server_config_default() {
        let config = HcfsClientConfig::default();
        assert!(config.base_url.contains("localhost"));
        assert!(!config.api_key.is_empty());
        assert!(!config.bearer_token.is_empty());
    }

    #[tokio::test]
    async fn test_get_state_response_parsing() {
        // Test that GetStateResult can be deserialized
        let json = r#"{
            "ss58_address": "test_user",
            "folder_hash": "",
            "files": [],
            "total_count": 0,
            "has_more": false,
            "offset": 0,
            "limit": 1000
        }"#;

        let response: GetStateResult = serde_json::from_str(json).unwrap();
        assert_eq!(response.ss58_address, "test_user");
        assert_eq!(response.total_count, 0);
        assert!(!response.has_more);
    }

    // =========================================================================
    // Mock Server Integration Tests
    // =========================================================================

    fn create_test_config(mock_server: &MockServer) -> HcfsClientConfig {
        HcfsClientConfig {
            base_url: mock_server.uri(),
            api_key: "test-api-key".to_string(),
            bearer_token: "test-bearer-token".to_string(),
            accept_invalid_certs: true,
            billing_bypass_token: None,
            ss58_address: String::new(),
            folder_hash: String::new(),
        }
    }

    #[tokio::test]
    async fn test_health_check_success() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path("/health"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "status": "healthy",
                "version": "0.1.0"
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.health_check().await;
        assert!(result.is_ok());
        assert!(result.unwrap());
    }

    #[tokio::test]
    async fn test_get_state_empty() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "test_user",
                    "folder_hash": "",
                    "files": [],
                    "total_count": 0,
                    "has_more": false,
                    "offset": 0,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.get_state("test_user", "", 0, 1000).await;
        assert!(result.is_ok());

        let state = result.unwrap();
        assert_eq!(state.ss58_address, "test_user");
        assert_eq!(state.files.len(), 0);
        assert_eq!(state.total_count, 0);
        assert!(!state.has_more);
    }

    #[tokio::test]
    async fn test_get_state_with_files() {
        let mock_server = MockServer::start().await;

        // Create test file entry with proper arrays
        let path_hash: Vec<u8> = vec![1u8; 32];
        let salted_hash: Vec<u8> = vec![2u8; 32];
        let revision_id: Vec<u8> = vec![3u8; 32];

        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "test_user",
                    "folder_hash": "",
                    "files": [{
                        "path_hash": path_hash,
                        "salted_hash": salted_hash,
                        "size_bytes": 1024,
                        "revision_seq": 1,
                        "revision_id": revision_id,
                        "encrypted_path": []
                    }],
                    "total_count": 1,
                    "has_more": false,
                    "offset": 0,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.get_state("test_user", "", 0, 1000).await;
        assert!(result.is_ok());

        let state = result.unwrap();
        assert_eq!(state.files.len(), 1);
        assert_eq!(state.total_count, 1);
        assert_eq!(state.files[0].size_bytes, 1024);
        assert_eq!(state.files[0].revision_seq, 1);
    }

    #[tokio::test]
    async fn test_get_state_user_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "nonexistent_user",
                    "folder_hash": "",
                    "files": [],
                    "total_count": 0,
                    "has_more": false,
                    "offset": 0,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client
            .get_state("nonexistent_user", "abcdef0123456789", 0, 1000)
            .await;
        assert!(result.is_ok());

        // User not found returns empty state
        let state = result.unwrap();
        assert_eq!(state.files.len(), 0);
        assert_eq!(state.total_count, 0);
    }

    #[tokio::test]
    async fn test_get_all_files_pagination() {
        let mock_server = MockServer::start().await;

        let path_hash1: Vec<u8> = vec![1u8; 32];
        let salted_hash1: Vec<u8> = vec![2u8; 32];
        let revision_id1: Vec<u8> = vec![3u8; 32];
        let path_hash2: Vec<u8> = vec![4u8; 32];
        let salted_hash2: Vec<u8> = vec![5u8; 32];
        let revision_id2: Vec<u8> = vec![6u8; 32];

        // First page
        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .and(query_param("offset", "0"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "test_user",
                    "folder_hash": "",
                    "files": [{
                        "path_hash": path_hash1,
                        "salted_hash": salted_hash1,
                        "size_bytes": 100,
                        "revision_seq": 1,
                        "revision_id": revision_id1,
                        "encrypted_path": []
                    }],
                    "total_count": 2,
                    "has_more": true,
                    "offset": 0,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        // Second page
        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .and(query_param("offset", "1000"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "ss58_address": "test_user",
                    "folder_hash": "",
                    "files": [{
                        "path_hash": path_hash2,
                        "salted_hash": salted_hash2,
                        "size_bytes": 200,
                        "revision_seq": 1,
                        "revision_id": revision_id2,
                        "encrypted_path": []
                    }],
                    "total_count": 2,
                    "has_more": false,
                    "offset": 1000,
                    "limit": 1000
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client
            .get_all_files("test_user", "", None::<fn(u64, u64)>)
            .await;
        assert!(result.is_ok());

        let files = result.unwrap();
        assert_eq!(files.len(), 2);
        assert_eq!(files[0].size_bytes, 100);
        assert_eq!(files[1].size_bytes, 200);
    }

    #[tokio::test]
    async fn test_download_success() {
        let mock_server = MockServer::start().await;

        let ciphertext = b"encrypted_content";
        let revision_id = [42u8; 32];

        Mock::given(method("GET"))
            .and(path_regex(r"/download/.*"))
            .respond_with(
                ResponseTemplate::new(200)
                    .set_body_bytes(ciphertext.to_vec())
                    .insert_header("X-Revision-Seq", "5")
                    .insert_header("X-Revision-Id", hex::encode(revision_id))
                    .insert_header("X-Size-Bytes", "17")
                    .insert_header("Content-Type", "application/octet-stream"),
            )
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        // Create temp file for download
        let temp_dir = std::env::temp_dir();
        let output_path = temp_dir.join("test_download_output");

        let result = client
            .download(
                "test_user",
                "abcdef0123456789",
                "abc123",
                &output_path,
                None::<fn(u64, u64)>,
            )
            .await;
        assert!(result.is_ok());

        let download = result.unwrap();
        assert_eq!(download.revision_seq, 5);
        assert_eq!(download.revision_id, revision_id);

        // Verify file contents
        let downloaded_content = std::fs::read(&output_path).unwrap();
        assert_eq!(downloaded_content, ciphertext);

        // Clean up
        let _ = std::fs::remove_file(&output_path);
    }

    #[tokio::test]
    async fn test_download_not_found() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path_regex(r"/download/.*"))
            .respond_with(ResponseTemplate::new(404).set_body_json(serde_json::json!({
                "error": "not_found",
                "message": "File not found"
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        // Create temp file for download
        let temp_dir = std::env::temp_dir();
        let output_path = temp_dir.join("test_download_not_found");

        let result = client
            .download(
                "test_user",
                "abcdef0123456789",
                "nonexistent",
                &output_path,
                None::<fn(u64, u64)>,
            )
            .await;
        assert!(result.is_err());

        match result {
            Err(SyncError::FileNotFound(_)) => {}
            _ => panic!("Expected FileNotFound error"),
        }
    }

    #[tokio::test]
    async fn test_delete_success() {
        let mock_server = MockServer::start().await;

        Mock::given(method("DELETE"))
            .and(path_regex(r"/delete/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "status": "deleted",
                    "file_id": "abc123",
                    "ss58_address": "test_user",
                    "folder_hash": ""
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.delete("test_user", "", "abc123").await;
        assert!(result.is_ok());

        let response = result.unwrap();
        assert_eq!(response.status, "deleted");
        assert_eq!(response.file_id, "abc123");
    }

    #[tokio::test]
    async fn test_delete_not_found_is_ok() {
        let mock_server = MockServer::start().await;

        // Server returns Success with status "not_found" for idempotent delete
        Mock::given(method("DELETE"))
            .and(path_regex(r"/delete/.*"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "status": "not_found",
                    "file_id": "nonexistent",
                    "ss58_address": "test_user",
                    "folder_hash": ""
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        // Delete of non-existent file should succeed (idempotent)
        let result = client.delete("test_user", "", "nonexistent").await;
        assert!(result.is_ok());

        let response = result.unwrap();
        assert_eq!(response.status, "not_found");
    }

    #[tokio::test]
    async fn test_upload_success() {
        let mock_server = MockServer::start().await;

        let revision_id: Vec<u8> = vec![99u8; 32];

        Mock::given(method("POST"))
            .and(path("/upload"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "Success": {
                    "upload_id": "upload-123",
                    "bytes_accepted": 100,
                    "timestamp": 1234567890,
                    "revision_id": revision_id
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        // Create temp file for upload
        let temp_dir = std::env::temp_dir();
        let ciphertext_path = temp_dir.join("test_upload_ciphertext");
        {
            let mut file = std::fs::File::create(&ciphertext_path).unwrap();
            file.write_all(&vec![0u8; 100]).unwrap();
        }

        let manifest = Manifest {
            ss58_address: "test_user".to_string(),
            folder_hash: String::new(),
            ciphertext_hash: "hash123".to_string(),
            size_bytes: 100,
            timestamp: 1234567890,
            signature: [0u8; 64],
            signing_key: [0u8; 32],
            path_hash: [1u8; 32],
            salted_hash: [2u8; 32],
            revision_seq: 1,
            base_revision_id: None,
            encrypted_path: vec![],
            file_name: None,
        };

        let result = client
            .upload(manifest, &ciphertext_path, None::<fn(u64, u64)>)
            .await;

        // Clean up
        let _ = std::fs::remove_file(&ciphertext_path);

        assert!(result.is_ok());

        let response = result.unwrap();
        assert_eq!(response.revision_id, [99u8; 32]);
    }

    #[tokio::test]
    async fn test_upload_conflict() {
        let mock_server = MockServer::start().await;

        let current_revision_id: Vec<u8> = vec![88u8; 32];

        Mock::given(method("POST"))
            .and(path("/upload"))
            .respond_with(ResponseTemplate::new(409).set_body_json(serde_json::json!({
                "Conflict": {
                    "error": "conflict",
                    "message": "Base revision does not match current",
                    "current_revision_id": current_revision_id,
                    "current_revision_seq": 5
                }
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        // Create temp file for upload
        let temp_dir = std::env::temp_dir();
        let ciphertext_path = temp_dir.join("test_upload_conflict_ciphertext");
        {
            let mut file = std::fs::File::create(&ciphertext_path).unwrap();
            file.write_all(&vec![0u8; 100]).unwrap();
        }

        let manifest = Manifest {
            ss58_address: "test_user".to_string(),
            folder_hash: String::new(),
            ciphertext_hash: "hash123".to_string(),
            size_bytes: 100,
            timestamp: 1234567890,
            signature: [0u8; 64],
            signing_key: [0u8; 32],
            path_hash: [1u8; 32],
            salted_hash: [2u8; 32],
            revision_seq: 1,
            base_revision_id: Some([0u8; 32]), // Wrong base revision
            encrypted_path: vec![],
            file_name: None,
        };

        let result = client
            .upload(manifest, &ciphertext_path, None::<fn(u64, u64)>)
            .await;

        // Clean up
        let _ = std::fs::remove_file(&ciphertext_path);

        assert!(result.is_err());

        match result {
            Err(SyncError::Conflict {
                file_id,
                current_revision_id: rev,
                message,
            }) => {
                assert_eq!(file_id, [1u8; 32]);
                assert_eq!(rev, [88u8; 32]);
                assert!(message.contains("revision"));
            }
            _ => panic!("Expected Conflict error"),
        }
    }

    // =========================================================================
    // Middleware Error Tests (non-NetworkResponse bodies)
    // =========================================================================

    #[tokio::test]
    async fn test_get_state_rate_limited() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(
                ResponseTemplate::new(429)
                    .insert_header("Retry-After", "30")
                    .set_body_json(serde_json::json!({
                        "error": "rate_limit_exceeded",
                        "message": "Too many requests.",
                        "retry_after_secs": 30
                    })),
            )
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.get_state("test_user", "", 0, 1000).await;
        assert!(result.is_err());

        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("429") || err.contains("rate_limit"),
            "Error should mention status or rate limit, got: {err}"
        );
    }

    #[tokio::test]
    async fn test_get_state_unauthorized_no_body() {
        let mock_server = MockServer::start().await;

        Mock::given(method("GET"))
            .and(path_regex(r"/get_state/.*"))
            .respond_with(ResponseTemplate::new(401))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.get_state("test_user", "", 0, 1000).await;
        assert!(result.is_err());

        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("401"),
            "Error should mention 401 status, got: {err}"
        );
    }

    #[tokio::test]
    async fn test_delete_rate_limited() {
        let mock_server = MockServer::start().await;

        Mock::given(method("DELETE"))
            .and(path_regex(r"/delete/.*"))
            .respond_with(ResponseTemplate::new(429).set_body_json(serde_json::json!({
                "error": "rate_limit_exceeded",
                "message": "Too many requests."
            })))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let result = client.delete("test_user", "", "abc123").await;
        assert!(result.is_err());

        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("429") || err.contains("rate_limit"),
            "Error should mention status or rate limit, got: {err}"
        );
    }

    #[tokio::test]
    async fn test_upload_unauthorized_no_body() {
        let mock_server = MockServer::start().await;

        Mock::given(method("POST"))
            .and(path("/upload"))
            .respond_with(ResponseTemplate::new(401))
            .mount(&mock_server)
            .await;

        let config = create_test_config(&mock_server);
        let client = HcfsClient::new(config).unwrap();

        let temp_dir = std::env::temp_dir();
        let ciphertext_path = temp_dir.join("test_upload_unauth_ciphertext");
        {
            let mut file = std::fs::File::create(&ciphertext_path).unwrap();
            file.write_all(&vec![0u8; 100]).unwrap();
        }

        let manifest = Manifest {
            ss58_address: "test_user".to_string(),
            folder_hash: String::new(),
            ciphertext_hash: "hash123".to_string(),
            size_bytes: 100,
            timestamp: 1234567890,
            signature: [0u8; 64],
            signing_key: [0u8; 32],
            path_hash: [1u8; 32],
            salted_hash: [2u8; 32],
            revision_seq: 1,
            base_revision_id: None,
            encrypted_path: vec![],
            file_name: None,
        };

        let result = client
            .upload(manifest, &ciphertext_path, None::<fn(u64, u64)>)
            .await;

        let _ = std::fs::remove_file(&ciphertext_path);

        assert!(result.is_err());

        let err = result.unwrap_err().to_string();
        assert!(
            err.contains("401"),
            "Error should mention 401 status, got: {err}"
        );
    }
}
