//! Chain information convenience methods.

use sui_graphql_macros::Response;

use super::Client;
use crate::error::Error;
use crate::scalars::BigInt;
use crate::scalars::DateTime;
use crate::scalars::Digest;

/// Information about an epoch.
///
/// This struct is consistent with the TypeScript SDK's `EpochInfo` and
/// the gRPC `Epoch` type from sui-rpc.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Epoch {
    /// The epoch's id as a sequence number starting at 0.
    pub epoch: u64,
    /// The first checkpoint in this epoch.
    pub first_checkpoint: Option<u64>,
    /// The last checkpoint in this epoch (None if epoch is ongoing).
    pub last_checkpoint: Option<u64>,
    /// Timestamp when this epoch started.
    pub epoch_start_timestamp: Option<DateTime>,
    /// Timestamp when this epoch ended (None if ongoing).
    pub epoch_end_timestamp: Option<DateTime>,
    /// The total number of transactions in this epoch.
    pub epoch_total_transactions: Option<u64>,
    /// Reference gas price in MIST for this epoch.
    pub reference_gas_price: Option<u64>,
    /// The protocol version for this epoch.
    pub protocol_version: Option<u64>,
}

impl Client {
    /// Get the chain identifier (e.g., "35834a8a" for mainnet).
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sui_graphql::Client;
    ///
    /// let client = Client::new("https://graphql.mainnet.sui.io/graphql")?;
    /// let chain_id = client.chain_identifier().await?;
    /// println!("Connected to chain: {}", chain_id);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn chain_identifier(&self) -> Result<Digest, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "chainIdentifier?")]
            chain_identifier: Option<Digest>,
        }

        const QUERY: &str = "query { chainIdentifier }";

        let response = self.query::<Response>(QUERY, serde_json::json!({})).await?;

        response
            .into_data()
            .and_then(|d| d.chain_identifier)
            .ok_or(Error::MissingData("chain identifier"))
    }

    /// Get the current protocol version.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sui_graphql::Client;
    ///
    /// let client = Client::new("https://graphql.mainnet.sui.io/graphql")?;
    /// let version = client.protocol_version().await?;
    /// println!("Protocol version: {}", version);
    /// # Ok(())
    /// # }
    /// ```
    pub async fn protocol_version(&self) -> Result<u64, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "protocolConfigs?.protocolVersion?")]
            protocol_version: Option<u64>,
        }

        const QUERY: &str = "query { protocolConfigs { protocolVersion } }";

        let response = self.query::<Response>(QUERY, serde_json::json!({})).await?;

        response
            .into_data()
            .and_then(|d| d.protocol_version)
            .ok_or(Error::MissingData("protocol version"))
    }

    /// Get epoch information by ID, or the current epoch if no ID is provided.
    ///
    /// Returns `None` if the epoch does not exist or was pruned.
    ///
    /// # Example
    ///
    /// ```no_run
    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
    /// use sui_graphql::Client;
    ///
    /// let client = Client::new("https://graphql.mainnet.sui.io/graphql")?;
    ///
    /// // Get current epoch
    /// let epoch = client.epoch(None).await?;
    ///
    /// // Get specific epoch
    /// let epoch = client.epoch(Some(100)).await?;
    /// # Ok(())
    /// # }
    /// ```
    pub async fn epoch(&self, epoch_id: Option<u64>) -> Result<Option<Epoch>, Error> {
        #[derive(Response)]
        struct Response {
            #[field(path = "epoch?.epochId?")]
            epoch_id: Option<u64>,
            #[field(path = "epoch?.protocolConfigs?.protocolVersion?")]
            protocol_version: Option<u64>,
            #[field(path = "epoch?.referenceGasPrice?")]
            reference_gas_price: Option<BigInt>,
            #[field(path = "epoch?.startTimestamp?")]
            start_timestamp: Option<DateTime>,
            #[field(path = "epoch?.endTimestamp?")]
            end_timestamp: Option<DateTime>,
            #[field(path = "epoch?.totalTransactions?")]
            total_transactions: Option<u64>,
            // Use alias syntax matching GraphQL: "alias:field" where alias comes first
            // e.g., "firstCheckpoint:checkpoints" validates against "checkpoints" schema
            // but extracts from "firstCheckpoint" in JSON (the aliased name in the query)
            #[field(path = "epoch?.firstCheckpoint:checkpoints?.nodes?[].sequenceNumber")]
            first_checkpoint_seq: Option<Vec<u64>>,
            // TODO use nodes[0] once we have support for it
            #[field(path = "epoch?.lastCheckpoint:checkpoints?.nodes?[].sequenceNumber")]
            last_checkpoint_seq: Option<Vec<u64>>,
        }

        const QUERY: &str = r#"
            query($epochId: UInt53) {
                epoch(epochId: $epochId) {
                    epochId
                    protocolConfigs {
                        protocolVersion
                    }
                    referenceGasPrice
                    startTimestamp
                    endTimestamp
                    totalTransactions
                    firstCheckpoint: checkpoints(first: 1) {
                        nodes {
                            sequenceNumber
                        }
                    }
                    lastCheckpoint: checkpoints(last: 1) {
                        nodes {
                            sequenceNumber
                        }
                    }
                }
            }
        "#;

        let variables = serde_json::json!({
            "epochId": epoch_id,
        });

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

        let Some(data) = response.into_data() else {
            return Ok(None);
        };

        let Some(epoch) = data.epoch_id else {
            return Ok(None);
        };

        let reference_gas_price = data.reference_gas_price.map(|b| b.0);

        // Extract first/last checkpoint from the nested queries
        let first_checkpoint = data.first_checkpoint_seq.and_then(|v| v.first().copied());
        let last_checkpoint = data.last_checkpoint_seq.and_then(|v| v.first().copied());

        Ok(Some(Epoch {
            epoch,
            first_checkpoint,
            last_checkpoint,
            epoch_start_timestamp: data.start_timestamp,
            epoch_end_timestamp: data.end_timestamp,
            epoch_total_transactions: data.total_transactions,
            reference_gas_price,
            protocol_version: data.protocol_version,
        }))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use wiremock::Mock;
    use wiremock::MockServer;
    use wiremock::ResponseTemplate;
    use wiremock::matchers::method;
    use wiremock::matchers::path;

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

        // Use a valid Base58 encoded 32-byte digest
        let expected_digest = Digest::ZERO;

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

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.chain_identifier().await;

        assert!(result.is_ok());
        assert_eq!(result.unwrap(), expected_digest);
    }

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

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

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.protocol_version().await;

        assert!(result.is_ok());
        assert_eq!(result.unwrap(), 70);
    }

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

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

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.protocol_version().await;

        assert!(result.is_err());
        assert!(matches!(result, Err(Error::MissingData(_))));
    }

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "epoch": {
                        "epochId": 500,
                        "protocolConfigs": {
                            "protocolVersion": 70
                        },
                        "referenceGasPrice": "1000",
                        "startTimestamp": "2024-01-15T00:00:00Z",
                        "endTimestamp": null,
                                                "totalTransactions": 987654,
                        "firstCheckpoint": {
                            "nodes": [{ "sequenceNumber": 10000 }]
                        },
                        "lastCheckpoint": {
                            "nodes": [{ "sequenceNumber": 22344 }]
                        }
                    }
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.epoch(None).await;

        assert!(result.is_ok());
        let epoch = result.unwrap();
        assert!(epoch.is_some());

        let epoch = epoch.unwrap();
        assert_eq!(epoch.epoch, 500);
        assert_eq!(epoch.protocol_version, Some(70));
        assert_eq!(epoch.reference_gas_price, Some(1000));
        assert_eq!(epoch.epoch_total_transactions, Some(987654));
        assert_eq!(epoch.first_checkpoint, Some(10000));
        assert_eq!(epoch.last_checkpoint, Some(22344));
    }

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "epoch": {
                        "epochId": 100,
                        "protocolConfigs": {
                            "protocolVersion": 50
                        },
                        "referenceGasPrice": "750",
                        "startTimestamp": "2023-06-01T00:00:00Z",
                        "endTimestamp": "2023-06-02T00:00:00Z",
                                                "totalTransactions": 100000,
                        "firstCheckpoint": {
                            "nodes": [{ "sequenceNumber": 1000 }]
                        },
                        "lastCheckpoint": {
                            "nodes": [{ "sequenceNumber": 5999 }]
                        }
                    }
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.epoch(Some(100)).await;

        assert!(result.is_ok());
        let epoch = result.unwrap();
        assert!(epoch.is_some());

        let epoch = epoch.unwrap();
        assert_eq!(epoch.epoch, 100);
        assert_eq!(epoch.protocol_version, Some(50));
        assert_eq!(epoch.reference_gas_price, Some(750));
        assert_eq!(epoch.epoch_total_transactions, Some(100000));
        assert_eq!(epoch.first_checkpoint, Some(1000));
        assert_eq!(epoch.last_checkpoint, Some(5999));
    }

    // Note: test_epoch_not_found is omitted because the current macro doesn't support
    // nullable parent paths with array fields. When epoch is null, the checkpoint
    // extraction fails. This limitation will be addressed in a future update.

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

        Mock::given(method("POST"))
            .and(path("/"))
            .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
                "data": {
                    "epoch": {
                        "epochId": 100,
                        "protocolConfigs": {
                            "protocolVersion": 50
                        },
                        "referenceGasPrice": "1000",
                        "startTimestamp": "2024-01-15T00:00:00Z",
                        "endTimestamp": "2024-01-16T00:00:00.123Z",
                                                "totalTransactions": 100000,
                        "firstCheckpoint": {
                            "nodes": [{ "sequenceNumber": 1000 }]
                        },
                        "lastCheckpoint": {
                            "nodes": [{ "sequenceNumber": 5999 }]
                        }
                    }
                }
            })))
            .mount(&mock_server)
            .await;

        let client = Client::new(&mock_server.uri()).unwrap();
        let result = client.epoch(Some(100)).await;

        assert!(result.is_ok());
        let epoch = result.unwrap().unwrap();

        // Verify timestamps are parsed as DateTime
        assert_eq!(
            epoch.epoch_start_timestamp,
            Some("2024-01-15T00:00:00Z".parse::<DateTime>().unwrap())
        );
        assert_eq!(
            epoch.epoch_end_timestamp,
            Some("2024-01-16T00:00:00.123Z".parse::<DateTime>().unwrap())
        );
    }
}
