// Copyright (c) Subzero Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use rialo_s_sdk::{
    signature::Signature,
    transaction::{Transaction, VersionedTransaction},
};

use crate::{
    generated::{rpc_client::RpcClient, types::SendTransactionOptions},
    RialoError,
};

pub struct TransactionSender<'a> {
    rpc_client: &'a dyn RpcClient,

    options: Option<SendTransactionOptions>,

    transaction: &'a Transaction,
}

impl<'a> TransactionSender<'a> {
    pub fn new(transaction: &'a Transaction, rpc_client: &'a dyn RpcClient) -> Self {
        Self {
            rpc_client,
            options: None,
            transaction,
        }
    }

    pub fn with_options(mut self, options: SendTransactionOptions) -> Self {
        self.options = Some(options);
        self
    }

    pub async fn send(self) -> Result<Signature, RialoError> {
        let serialized_tx = bincode::serialize(&self.transaction).map_err(|err| {
            RialoError::SerializationError(format!("Failed to serialize transaction: {err}"))
        })?;

        self.rpc_client
            .send_transaction(&serialized_tx, self.options)
            .await
    }
}

pub async fn send_versioned_transaction(
    rpc_client: &impl RpcClient,
    transaction: &VersionedTransaction,
) -> Result<Signature, RialoError> {
    let serialized_tx = bincode::serialize(transaction).map_err(|err| {
        RialoError::SerializationError(format!("Failed to serialize transaction: {err}"))
    })?;

    rpc_client.send_transaction(&serialized_tx, None).await
}

pub async fn send_versioned_transaction_with_options(
    rpc_client: &impl RpcClient,
    transaction: &VersionedTransaction,
    options: SendTransactionOptions,
) -> Result<Signature, RialoError> {
    let serialized_tx = bincode::serialize(transaction).map_err(|err| {
        RialoError::SerializationError(format!("Failed to serialize transaction: {err}"))
    })?;

    rpc_client
        .send_transaction(&serialized_tx, Some(options))
        .await
}

pub async fn get_transaction_details(
    rpc_client: &impl RpcClient,
    signature: &str,
) -> Result<crate::generated::types::TransactionResponse, RialoError> {
    use std::str::FromStr;
    let sig = Signature::from_str(signature)
        .map_err(|e| RialoError::SerializationError(format!("Invalid signature: {e}")))?;

    rpc_client.get_transaction(&sig).await
}

pub async fn get_transaction_status(
    rpc_client: &impl RpcClient,
    signature: &str,
) -> Result<Vec<Option<crate::generated::types::SignatureStatus>>, RialoError> {
    use std::str::FromStr;
    let sig = Signature::from_str(signature)
        .map_err(|e| RialoError::SerializationError(format!("Invalid signature: {e}")))?;

    rpc_client.get_signature_statuses(&[sig]).await
}

#[cfg(test)]
mod tests {
    use rialo_s_sdk::{
        pubkey::Pubkey,
        signature::{Keypair, Signer},
        system_instruction,
    };

    use super::*;
    use crate::{
        constants::ConfigHashPrefix,
        rpc::mock_client::{MockRpcClient, ERROR_MESSAGE, MOCK_SIGNATURE},
    };

    #[tokio::test]
    async fn test_send_versioned_transaction_success() {
        use std::str::FromStr;
        let mock_rpc =
            MockRpcClient::new().with_signature(Signature::from_str(MOCK_SIGNATURE).unwrap());

        let keypair = Keypair::new();
        let recipient = Pubkey::new_unique();
        let instruction = system_instruction::transfer(&keypair.pubkey(), &recipient, 1000);
        let transaction = rialo_s_sdk::v0_message_utils::create_v0_transaction(
            &[&keypair],
            &[instruction],
            &keypair.pubkey(),
            0,
            ConfigHashPrefix::new(1),
            false,
        )
        .expect("Failed to create transaction");

        let result = send_versioned_transaction(&mock_rpc, &transaction).await;

        assert!(result.is_ok());
        let signature = result.unwrap();
        assert_eq!(signature.to_string(), MOCK_SIGNATURE);
    }

    #[tokio::test]
    async fn test_send_versioned_transaction_error() {
        let mock_rpc = MockRpcClient::new()
            .with_error("Transaction simulation failed: Error processing Instruction 0: custom program error: 0x1".to_string());

        let keypair = Keypair::new();
        let recipient = Pubkey::new_unique();
        let instruction = system_instruction::transfer(&keypair.pubkey(), &recipient, 1000);
        let transaction = rialo_s_sdk::v0_message_utils::create_v0_transaction(
            &[&keypair],
            &[instruction],
            &keypair.pubkey(),
            0,
            ConfigHashPrefix::new(1),
            false,
        )
        .expect("Failed to create transaction");

        let result = send_versioned_transaction(&mock_rpc, &transaction).await;

        assert!(result.is_err());
        match result {
            Err(RialoError::Rpc(msg)) => {
                assert!(msg.message.contains("Transaction simulation failed"));
            }
            _ => panic!("Expected RpcError but got different error type"),
        }
    }

    #[tokio::test]
    async fn test_get_transaction_details_success() {
        use std::str::FromStr;

        use crate::generated::types::*;

        let test_transaction = TransactionResponse {
            block_height: 100,
            block_time: None,
            meta: TransactionStatusMetadata {
                fee: 5000,
                err: None,
                log_messages: None,
            },
            transaction: TransactionData {
                signatures: vec![
                    rialo_s_sdk::signature::Signature::from_str(MOCK_SIGNATURE).unwrap()
                ],
                message: TransactionMessage {
                    header: MessageHeader {
                        num_required_signatures: 1,
                        num_readonly_signed_accounts: 0,
                        num_readonly_unsigned_accounts: 1,
                    },
                    account_keys: vec![Pubkey::new_unique(), rialo_s_sdk::system_program::id()],
                    instructions: vec![],
                },
                valid_from: 0,
            },
        };

        let mock_rpc = MockRpcClient::new().with_transaction(test_transaction);

        let result = get_transaction_details(&mock_rpc, MOCK_SIGNATURE).await;

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

        assert_eq!(transaction.transaction.signatures.len(), 1);
        assert_eq!(
            transaction
                .transaction
                .message
                .header
                .num_required_signatures,
            1
        );
    }

    #[tokio::test]
    async fn test_get_transaction_details_error() {
        let mock_rpc = MockRpcClient::new().with_error(ERROR_MESSAGE.to_string());

        let result = get_transaction_details(&mock_rpc, MOCK_SIGNATURE).await;

        assert!(result.is_err());
        match result {
            Err(RialoError::Rpc(msg)) => {
                assert!(msg.message.contains(ERROR_MESSAGE));
            }
            _ => panic!("Expected RpcError but got different error type"),
        }
    }

    #[tokio::test]
    async fn test_get_transaction_status_success() {
        let mock_rpc = MockRpcClient::new();

        let result = get_transaction_status(&mock_rpc, MOCK_SIGNATURE).await;

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

        // Check that we get the signature statuses vector (MockRpcClient returns 1 status by default)
        assert_eq!(statuses.len(), 1);
        let status = statuses[0].as_ref().expect("Status should be Some");
        assert!(status.executed); // Default MockRpcClient status
    }

    #[tokio::test]
    async fn test_get_transaction_status_error() {
        let mock_rpc = MockRpcClient::new().with_error(ERROR_MESSAGE.to_string());

        let result = get_transaction_status(&mock_rpc, MOCK_SIGNATURE).await;

        assert!(result.is_err());
        match result {
            Err(RialoError::Rpc(msg)) => {
                assert!(msg.message.contains(ERROR_MESSAGE));
            }
            _ => panic!("Expected RpcError but got different error type"),
        }
    }
}
