//! Object-related convenience methods.

use futures::Stream;
use sui_graphql_macros::Response;
use sui_sdk_types::Address;
use sui_sdk_types::Object;

use super::Client;
use crate::bcs::Bcs;
use crate::error::Error;
use crate::pagination::Page;
use crate::pagination::PageInfo;
use crate::pagination::paginate;

impl Client {
    /// Fetch an object by its ID and deserialize from BCS.
    ///
    /// Returns:
    /// - `Ok(Some(object))` if the object exists
    /// - `Ok(None)` if the object does not exist
    /// - `Err(Error::Request)` for network errors
    /// - `Err(Error::Base64)` / `Err(Error::Bcs)` for decoding errors
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sui_graphql::Client;
    /// use sui_sdk_types::Address;
    ///
    /// let client = Client::new(Client::MAINNET)?;
    /// let object_id: Address = "0x5".parse()?;
    ///
    /// match client.get_object(object_id).await? {
    ///     Some(object) => println!("Object version: {}", object.version()),
    ///     None => println!("Object not found"),
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub async fn get_object(&self, object_id: Address) -> Result<Option<Object>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "object?.objectBcs?")]
            object: Option<Bcs<Object>>,
        }

        const QUERY: &str = r#"
            query($id: SuiAddress!) {
                object(address: $id) {
                    objectBcs
                }
            }
        "#;

        let variables = serde_json::json!({ "id": object_id });

        let response = self.query::<Response>(QUERY, variables).await?;

        Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
    }

    /// Fetch an object at a specific version.
    pub async fn get_object_at_version(
        &self,
        object_id: Address,
        version: u64,
    ) -> Result<Option<Object>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "object?.objectBcs?")]
            object: Option<Bcs<Object>>,
        }

        const QUERY: &str = r#"
            query($id: SuiAddress!, $version: UInt53) {
                object(address: $id, version: $version) {
                    objectBcs
                }
            }
        "#;

        let variables = serde_json::json!({
            "id": object_id,
            "version": version,
        });

        let response = self.query::<Response>(QUERY, variables).await?;

        Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
    }

    /// Fetch an object at a specific checkpoint.
    ///
    /// Returns the object's state as of the given checkpoint.
    pub async fn get_object_at_checkpoint(
        &self,
        object_id: Address,
        checkpoint: u64,
    ) -> Result<Option<Object>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "object?.objectBcs?")]
            object: Option<Bcs<Object>>,
        }

        const QUERY: &str = r#"
            query($id: SuiAddress!, $atCheckpoint: UInt53) {
                object(address: $id, atCheckpoint: $atCheckpoint) {
                    objectBcs
                }
            }
        "#;

        let variables = serde_json::json!({
            "id": object_id,
            "atCheckpoint": checkpoint,
        });

        let response = self.query::<Response>(QUERY, variables).await?;

        Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
    }

    /// Fetch an object with a root version bound.
    ///
    /// This is useful for fetching child or wrapped objects bounded by their
    /// root object's version. The object will be fetched at the latest version
    /// at or before the given root version.
    pub async fn get_object_with_root_version(
        &self,
        object_id: Address,
        root_version: u64,
    ) -> Result<Option<Object>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "object?.objectBcs?")]
            object: Option<Bcs<Object>>,
        }

        const QUERY: &str = r#"
            query($id: SuiAddress!, $rootVersion: UInt53) {
                object(address: $id, rootVersion: $rootVersion) {
                    objectBcs
                }
            }
        "#;

        let variables = serde_json::json!({
            "id": object_id,
            "rootVersion": root_version,
        });

        let response = self.query::<Response>(QUERY, variables).await?;

        Ok(response.into_data().and_then(|d| d.object).map(|b| b.0))
    }

    /// Stream all objects owned by an address.
    ///
    /// Handles pagination automatically, fetching pages as needed.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use futures::StreamExt;
    /// use std::pin::pin;
    /// use sui_graphql::Client;
    /// use sui_sdk_types::Address;
    ///
    /// let client = Client::new(Client::TESTNET)?;
    /// let owner: Address = "0x123...".parse()?;
    ///
    /// let mut stream = pin!(client.list_objects(owner));
    /// while let Some(result) = stream.next().await {
    ///     let object = result?;
    ///     println!("Object version: {}", object.version());
    /// }
    /// # Ok(())
    /// # }
    /// ```
    pub fn list_objects(&self, owner: Address) -> impl Stream<Item = Result<Object, Error>> + '_ {
        let client = self.clone();
        paginate(move |cursor| {
            let client = client.clone();
            async move { client.fetch_objects_page(owner, cursor.as_deref()).await }
        })
    }

    /// Fetch a single page of objects owned by an address.
    async fn fetch_objects_page(
        &self,
        owner: Address,
        cursor: Option<&str>,
    ) -> Result<Page<Object>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "objects?.pageInfo?")]
            page_info: Option<PageInfo>,
            #[field(path = "objects?.nodes?[].objectBcs")]
            objects: Option<Vec<Bcs<Object>>>,
        }

        const QUERY: &str = r#"
            query($owner: SuiAddress!, $after: String) {
                objects(filter: { owner: $owner }, after: $after) {
                    pageInfo {
                        hasNextPage
                        endCursor
                    }
                    nodes {
                        objectBcs
                    }
                }
            }
        "#;

        let variables = serde_json::json!({
            "owner": owner,
            "after": cursor,
        });

        let response = self.query::<Response>(QUERY, variables).await?;

        let data = response.into_data();
        let page_info = data
            .as_ref()
            .and_then(|d| d.page_info.clone())
            .unwrap_or_default();

        let objects = data
            .and_then(|d| d.objects)
            .unwrap_or_default()
            .into_iter()
            .map(|b| b.0)
            .collect();

        Ok(Page {
            items: objects,
            has_next_page: page_info.has_next_page,
            end_cursor: page_info.end_cursor,
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use futures::StreamExt;
    use std::sync::Arc;
    use std::sync::atomic::AtomicUsize;
    use std::sync::atomic::Ordering;
    use wiremock::Mock;
    use wiremock::MockServer;
    use wiremock::ResponseTemplate;
    use wiremock::matchers::method;
    use wiremock::matchers::path;

    /// BCS-encoded SUI coin from sui-sdk-types test fixtures, encoded as base64.
    fn test_object_bcs() -> String {
        use base64ct::Base64;
        use base64ct::Encoding;

        // From sui-sdk-types/src/object.rs test fixtures (SUI_COIN)
        const SUI_COIN_BCS: &[u8] = &[
            0, 1, 1, 32, 79, 43, 0, 0, 0, 0, 0, 40, 35, 95, 175, 213, 151, 87, 206, 190, 35, 131,
            79, 35, 254, 22, 15, 181, 40, 108, 28, 77, 68, 229, 107, 254, 191, 160, 196, 186, 42,
            2, 122, 53, 52, 133, 199, 58, 0, 0, 0, 0, 0, 79, 255, 208, 0, 85, 34, 190, 75, 192, 41,
            114, 76, 127, 15, 110, 215, 9, 58, 107, 243, 160, 155, 144, 230, 47, 97, 220, 21, 24,
            30, 26, 62, 32, 17, 197, 192, 38, 64, 173, 142, 143, 49, 111, 15, 211, 92, 84, 48, 160,
            243, 102, 229, 253, 251, 137, 210, 101, 119, 173, 228, 51, 141, 20, 15, 85, 96, 19, 15,
            0, 0, 0, 0, 0,
        ];
        Base64::encode_string(SUI_COIN_BCS)
    }

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "object": null
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let object_id: Address = "0x5".parse().unwrap();

        let result = client.get_object(object_id).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_none());
    }

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "object": {
                        "objectBcs": test_object_bcs()
                    }
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let object_id: Address = "0x5".parse().unwrap();

        let result = client.get_object(object_id).await;
        assert!(result.is_ok());
        assert!(result.unwrap().is_some());
    }

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "objects": {
                        "pageInfo": {
                            "hasNextPage": false,
                            "endCursor": null
                        },
                        "nodes": []
                    }
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let owner: Address = "0x1".parse().unwrap();

        let stream = client.list_objects(owner);
        let objects: Vec<_> = futures::StreamExt::collect(stream).await;

        assert!(objects.is_empty());
    }

    #[tokio::test]
    async fn test_list_objects_with_pagination() {
        let mock_server = MockServer::start().await;
        let call_count = Arc::new(AtomicUsize::new(0));
        let call_count_clone = call_count.clone();

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(move |_req: &wiremock::Request| {
                let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
                match count {
                    // Page 1: 3 objects
                    0 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "data": {
                            "objects": {
                                "pageInfo": {
                                    "hasNextPage": true,
                                    "endCursor": "cursor1"
                                },
                                "nodes": [
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() }
                                ]
                            }
                        }
                    })),
                    // Page 2: 2 objects
                    1 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "data": {
                            "objects": {
                                "pageInfo": {
                                    "hasNextPage": false,
                                    "endCursor": null
                                },
                                "nodes": [
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() }
                                ]
                            }
                        }
                    })),
                    _ => ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "data": { "objects": { "pageInfo": { "hasNextPage": false, "endCursor": null }, "nodes": [] } }
                    })),
                }
            })
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let owner: Address = "0x1".parse().unwrap();

        let stream = client.list_objects(owner);
        let objects: Vec<_> = futures::StreamExt::collect(stream).await;

        // Should have fetched 5 objects across 2 pages (3 + 2)
        assert_eq!(objects.len(), 5);
        assert_eq!(call_count.load(Ordering::SeqCst), 2);

        for result in objects {
            assert!(result.is_ok());
        }
    }

    #[tokio::test]
    async fn test_list_objects_partial_consumption() {
        let mock_server = MockServer::start().await;
        let call_count = Arc::new(AtomicUsize::new(0));
        let call_count_clone = call_count.clone();

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(move |_req: &wiremock::Request| {
                let count = call_count_clone.fetch_add(1, Ordering::SeqCst);
                match count {
                    // Page 1: 3 objects
                    0 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "data": {
                            "objects": {
                                "pageInfo": {
                                    "hasNextPage": true,
                                    "endCursor": "cursor1"
                                },
                                "nodes": [
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() }
                                ]
                            }
                        }
                    })),
                    // Page 2: 2 objects
                    1 => ResponseTemplate::new(200).set_body_json(serde_json::json!({
                        "data": {
                            "objects": {
                                "pageInfo": {
                                    "hasNextPage": false,
                                    "endCursor": null
                                },
                                "nodes": [
                                    { "objectBcs": test_object_bcs() },
                                    { "objectBcs": test_object_bcs() }
                                ]
                            }
                        }
                    })),
                    _ => panic!("unexpected page request"),
                }
            })
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let owner: Address = "0x1".parse().unwrap();

        // Only take 3 objects out of 5 available
        let stream = client.list_objects(owner).take(3);
        let objects: Vec<_> = stream.collect().await;

        // Should have only fetched 3 objects from the first page
        assert_eq!(objects.len(), 3);
        assert_eq!(call_count.load(Ordering::SeqCst), 1);

        for result in objects {
            assert!(result.is_ok());
        }
    }
}
