//! In-memory mock S3 implementation for testing.
//!
//! No AWS credentials or network access required.

use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex};

use crate::error::S3boltError;
use crate::s3::ops::S3Operations;
use crate::types::{ObjectInfo, S3Uri};

/// An in-memory S3 mock. Objects are stored as `(bucket, key) -> MockObject`.
#[derive(Debug, Clone)]
pub struct MockS3 {
    store: Arc<Mutex<HashMap<(String, String), MockObject>>>,
    /// Track copy calls for assertion.
    pub copy_log: Arc<Mutex<Vec<(S3Uri, S3Uri)>>>,
}

#[derive(Debug, Clone)]
struct MockObject {
    size: u64,
    e_tag: String,
}

impl MockS3 {
    /// Create an empty mock store.
    pub fn new() -> Self {
        Self {
            store: Arc::new(Mutex::new(HashMap::new())),
            copy_log: Arc::new(Mutex::new(Vec::new())),
        }
    }

    /// Seed the mock with an object.
    pub fn put_object(&self, bucket: &str, key: &str, size: u64) {
        let e_tag = format!("\"etag-{key}\"");
        self.store.lock().unwrap().insert(
            (bucket.to_owned(), key.to_owned()),
            MockObject { size, e_tag },
        );
    }

    /// Seed multiple objects under a prefix with incrementing keys.
    pub fn put_objects(&self, bucket: &str, prefix: &str, count: usize, size: u64) {
        for i in 0..count {
            let key = format!("{prefix}file_{i:04}.parquet");
            self.put_object(bucket, &key, size);
        }
    }

    /// Check if an object exists at the given location.
    pub fn exists(&self, bucket: &str, key: &str) -> bool {
        self.store
            .lock()
            .unwrap()
            .contains_key(&(bucket.to_owned(), key.to_owned()))
    }

    /// Count objects in the store matching a bucket/prefix.
    pub fn count_objects(&self, bucket: &str, prefix: &str) -> usize {
        self.store
            .lock()
            .unwrap()
            .keys()
            .filter(|(b, k)| b == bucket && k.starts_with(prefix))
            .count()
    }

    /// Get the copy log (source, dest pairs).
    pub fn copies(&self) -> Vec<(S3Uri, S3Uri)> {
        self.copy_log.lock().unwrap().clone()
    }

    /// Internal copy logic shared by single and multipart copy.
    fn do_copy(
        store: &Arc<Mutex<HashMap<(String, String), MockObject>>>,
        copy_log: &Arc<Mutex<Vec<(S3Uri, S3Uri)>>>,
        source: &S3Uri,
        destination: &S3Uri,
    ) -> Result<String, S3boltError> {
        let source_obj = {
            let store = store.lock().unwrap();
            store
                .get(&(source.bucket.clone(), source.key.clone()))
                .cloned()
                .ok_or_else(|| S3boltError::S3Api {
                    operation: "CopyObject".to_owned(),
                    message: format!("source not found: {source}"),
                    is_retryable: false,
                })?
        };

        let dest_e_tag = format!("\"etag-copy-{}\"", destination.key);

        {
            let mut store = store.lock().unwrap();
            store.insert(
                (destination.bucket.clone(), destination.key.clone()),
                MockObject {
                    size: source_obj.size,
                    e_tag: dest_e_tag.clone(),
                },
            );
        }

        copy_log
            .lock()
            .unwrap()
            .push((source.clone(), destination.clone()));

        Ok(dest_e_tag)
    }
}

impl Default for MockS3 {
    fn default() -> Self {
        Self::new()
    }
}

impl S3Operations for MockS3 {
    fn list_objects(
        &self,
        uri: &S3Uri,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<ObjectInfo>, S3boltError>> + Send + '_>> {
        let uri = uri.clone();
        let store = self.store.clone();

        Box::pin(async move {
            let store = store.lock().unwrap();
            let objects: Vec<ObjectInfo> = store
                .iter()
                .filter(|((b, k), _)| b == &uri.bucket && k.starts_with(&uri.key))
                .map(|((_, k), obj)| ObjectInfo {
                    key: k.clone(),
                    size: obj.size,
                    e_tag: Some(obj.e_tag.clone()),
                    last_modified: None,
                    storage_class: None,
                })
                .collect();

            Ok(objects)
        })
    }

    fn copy_object(
        &self,
        source: &S3Uri,
        destination: &S3Uri,
        _storage_class: Option<&str>,
    ) -> Pin<Box<dyn Future<Output = Result<String, S3boltError>> + Send + '_>> {
        let source = source.clone();
        let destination = destination.clone();
        let store = self.store.clone();
        let copy_log = self.copy_log.clone();

        Box::pin(async move { Self::do_copy(&store, &copy_log, &source, &destination) })
    }

    fn copy_object_multipart(
        &self,
        source: &S3Uri,
        destination: &S3Uri,
        _object_size: u64,
        _part_size: u64,
        _storage_class: Option<&str>,
    ) -> Pin<Box<dyn Future<Output = Result<String, S3boltError>> + Send + '_>> {
        let source = source.clone();
        let destination = destination.clone();
        let store = self.store.clone();
        let copy_log = self.copy_log.clone();

        Box::pin(async move { Self::do_copy(&store, &copy_log, &source, &destination) })
    }

    fn head_object(
        &self,
        uri: &S3Uri,
    ) -> Pin<Box<dyn Future<Output = Result<Option<(String, u64)>, S3boltError>> + Send + '_>> {
        let uri = uri.clone();
        let store = self.store.clone();

        Box::pin(async move {
            let store = store.lock().unwrap();
            match store.get(&(uri.bucket.clone(), uri.key.clone())) {
                Some(obj) => Ok(Some((obj.e_tag.clone(), obj.size))),
                None => Ok(None),
            }
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn mock_list_objects() {
        let mock = MockS3::new();
        mock.put_objects("bucket", "data/", 5, 1024);

        let uri = S3Uri::parse("s3://bucket/data/").unwrap();
        let objects = mock.list_objects(&uri).await.unwrap();

        assert_eq!(objects.len(), 5);
        assert!(objects.iter().all(|o| o.size == 1024));
    }

    #[tokio::test]
    async fn mock_copy_object() {
        let mock = MockS3::new();
        mock.put_object("src", "file.txt", 100);

        let source = S3Uri::parse("s3://src/file.txt").unwrap();
        let dest = S3Uri::parse("s3://dst/file.txt").unwrap();

        let e_tag = mock.copy_object(&source, &dest, None).await.unwrap();
        assert!(!e_tag.is_empty());
        assert!(mock.exists("dst", "file.txt"));
        assert_eq!(mock.copies().len(), 1);
    }

    #[tokio::test]
    async fn mock_copy_nonexistent_fails() {
        let mock = MockS3::new();
        let source = S3Uri::parse("s3://src/missing.txt").unwrap();
        let dest = S3Uri::parse("s3://dst/missing.txt").unwrap();

        let result = mock.copy_object(&source, &dest, None).await;
        assert!(result.is_err());
    }

    #[tokio::test]
    async fn mock_head_object() {
        let mock = MockS3::new();
        mock.put_object("bucket", "exists.txt", 256);

        let found = S3Uri::parse("s3://bucket/exists.txt").unwrap();
        let missing = S3Uri::parse("s3://bucket/missing.txt").unwrap();

        let result = mock.head_object(&found).await.unwrap();
        assert!(result.is_some());
        let (_, size) = result.unwrap();
        assert_eq!(size, 256);

        let result = mock.head_object(&missing).await.unwrap();
        assert!(result.is_none());
    }

    #[tokio::test]
    async fn mock_list_with_prefix_filter() {
        let mock = MockS3::new();
        mock.put_object("bucket", "data/a.txt", 10);
        mock.put_object("bucket", "data/b.txt", 20);
        mock.put_object("bucket", "other/c.txt", 30);

        let uri = S3Uri::parse("s3://bucket/data/").unwrap();
        let objects = mock.list_objects(&uri).await.unwrap();

        assert_eq!(objects.len(), 2);
    }
}
