use super::ProofLogAdaptor;
use crate::envelope::PublishEnvelope;
#[cfg(feature = "solana-proof-log-rpc")]
use crate::error::TrazaeoError;
use crate::error::TrazaeoResult;
use crate::onchain::{
    commit_publish_proof_log, verify_proof_log_commitment_onchain, PublishProofLogCommitRequest,
};
#[cfg(feature = "solana-proof-log-rpc")]
use crate::proof_log::{
    build_proof_log_commitment, verify_proof_log_commitment_linkage, ProofLogReceipt,
};
use crate::proof_log::{ProofLogCommitment, ProofLogPublishResult};
use crate::solana::{
    cluster_name, get_anchor_account_by_pda, get_chain_root, get_transaction, program_id,
    SolanaClient,
};
use crate::utils::Hash;
#[cfg(feature = "solana-proof-log-rpc")]
use reqwest::blocking::Client;
#[cfg(feature = "solana-proof-log-rpc")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "solana-proof-log-rpc")]
use serde_json::json;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_rpc_client::rpc_client::RpcClient;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::commitment_config::CommitmentConfig;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::instruction::Instruction;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::message::Message;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::pubkey::Pubkey;
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::signature::{read_keypair_file, Keypair, Signer};
#[cfg(feature = "solana-proof-log-rpc")]
use solana_sdk::transaction::Transaction;
#[cfg(feature = "solana-proof-log-rpc")]
use std::str::FromStr;

#[derive(Debug, Clone)]
pub struct SolanaProofLogAdaptor {
    pub client: SolanaClient,
    pub attestor_pubkey: [u8; 32],
    pub attestor_key_ref: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg(feature = "solana-proof-log-rpc")]
pub struct PublicRpcSolanaProofLogConfig {
    pub rpc_url: String,
    pub cluster: String,
    pub memo_program_id: String,
    pub commitment: String,
    pub request_airdrop_on_devnet: bool,
}

#[cfg(feature = "solana-proof-log-rpc")]
pub struct PublicRpcSolanaProofLogAdaptor {
    pub config: PublicRpcSolanaProofLogConfig,
    rpc_client: RpcClient,
    http_client: Client,
    signer: Keypair,
}

#[cfg(feature = "solana-proof-log-rpc")]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct PublicRpcMemoPayload {
    #[serde(rename = "type")]
    payload_type: String,
    cluster: String,
    #[serde(rename = "committed_envelope_hash")]
    envelope_hash: String,
    #[serde(rename = "committed_checkpoint_hash")]
    checkpoint_hash: String,
    #[serde(rename = "committed_log_root_hash")]
    log_root_hash: String,
}

#[cfg(feature = "solana-proof-log-rpc")]
#[derive(Debug, Clone, PartialEq, Eq)]
struct PublicRpcTransactionProof {
    signatures: Vec<String>,
    signer_pubkeys: Vec<String>,
    program_id: String,
    memo_payload: PublicRpcMemoPayload,
}

#[cfg(feature = "solana-proof-log-rpc")]
impl PublicRpcSolanaProofLogAdaptor {
    fn extract_memo_payload_text(&self, instruction: &serde_json::Value) -> TrazaeoResult<String> {
        let parsed = instruction.get("parsed").ok_or_else(|| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                "memo payload not found in transaction",
            )
        })?;
        match parsed {
            serde_json::Value::String(value) => Ok(value.clone()),
            serde_json::Value::Object(_) => parsed
                .get("memo")
                .and_then(serde_json::Value::as_str)
                .map(str::to_string)
                .ok_or_else(|| {
                    TrazaeoError::external(
                        "public rpc proof log adaptor",
                        "memo payload not found in transaction",
                    )
                }),
            _ => Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "memo payload not found in transaction",
            )),
        }
    }

    fn build_config(rpc_url: &str, cluster: &str) -> PublicRpcSolanaProofLogConfig {
        PublicRpcSolanaProofLogConfig {
            rpc_url: rpc_url.to_string(),
            cluster: cluster.to_string(),
            memo_program_id: "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr".to_string(),
            commitment: "finalized".to_string(),
            request_airdrop_on_devnet: cluster == "solana-devnet",
        }
    }

    pub fn with_signer(rpc_url: &str, cluster: &str, signer: Keypair) -> Self {
        let commitment = CommitmentConfig::finalized();
        Self {
            config: Self::build_config(rpc_url, cluster),
            rpc_client: RpcClient::new_with_commitment(rpc_url.to_string(), commitment),
            http_client: Client::new(),
            signer,
        }
    }

    pub fn new(rpc_url: &str, cluster: &str) -> TrazaeoResult<Self> {
        if cluster == "solana-mainnet" {
            return Err(TrazaeoError::invalid_input(
                "public rpc proof log adaptor",
                "solana-mainnet requires an explicit funded keypair; use from_keypair_path",
            ));
        }
        Ok(Self::with_signer(rpc_url, cluster, Keypair::new()))
    }

    pub fn from_keypair_path(
        rpc_url: &str,
        cluster: &str,
        keypair_path: &str,
    ) -> TrazaeoResult<Self> {
        let signer = read_keypair_file(keypair_path).map_err(|e| {
            TrazaeoError::io(
                "public rpc proof log adaptor",
                format!("failed to read solana keypair file '{keypair_path}': {e}"),
            )
        })?;
        Ok(Self::with_signer(rpc_url, cluster, signer))
    }

    pub fn signer_pubkey(&self) -> String {
        self.signer.pubkey().to_string()
    }

    pub fn chain_root(&self) -> TrazaeoResult<Hash> {
        Err(TrazaeoError::external(
            "public rpc proof log adaptor",
            "chain root lookup is unsupported for memo-backed public RPC proof logs",
        ))
    }

    pub fn optional_chain_root(&self) -> Option<Hash> {
        None
    }

    fn memo_program_pubkey(&self) -> TrazaeoResult<Pubkey> {
        Pubkey::from_str(&self.config.memo_program_id).map_err(|e| {
            TrazaeoError::invalid_input(
                "public rpc proof log adaptor",
                format!("invalid memo program id: {e}"),
            )
        })
    }

    fn ensure_funded(&self) -> TrazaeoResult<()> {
        if !self.config.request_airdrop_on_devnet {
            return Ok(());
        }
        let balance = self
            .rpc_client
            .get_balance(&self.signer.pubkey())
            .map_err(|e| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    format!("failed to get balance: {e}"),
                )
            })?;
        if balance > 0 {
            return Ok(());
        }
        let signature = self
            .rpc_client
            .request_airdrop(&self.signer.pubkey(), 1_000_000_000)
            .map_err(|e| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    format!("failed to request airdrop: {e}"),
                )
            })?;
        self.rpc_client
            .confirm_transaction(&signature)
            .map_err(|e| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    format!("failed to confirm airdrop: {e}"),
                )
            })?;
        Ok(())
    }

    fn build_memo_payload(&self, envelope: &PublishEnvelope) -> String {
        let envelope_hash =
            hex::encode(blake3::hash(&envelope.canonical_signed_bytes()).as_bytes());
        json!({
            "type": "trazaeo_proof_log_commitment_v1",
            "cluster": self.config.cluster,
            "committed_envelope_hash": envelope_hash,
            "committed_checkpoint_hash": envelope.checkpoint_manifest_hash,
            "committed_log_root_hash": envelope.checkpoint_log_root_hash,
        })
        .to_string()
    }

    fn send_memo_transaction(&self, memo_payload: &str) -> TrazaeoResult<ProofLogReceipt> {
        self.ensure_funded()?;
        let blockhash = self.rpc_client.get_latest_blockhash().map_err(|e| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                format!("failed to get latest blockhash: {e}"),
            )
        })?;
        let instruction = Instruction::new_with_bytes(
            self.memo_program_pubkey()?,
            memo_payload.as_bytes(),
            vec![],
        );
        let message = Message::new(&[instruction], Some(&self.signer.pubkey()));
        let tx = Transaction::new(&[&self.signer], message, blockhash);
        let signature = self
            .rpc_client
            .send_and_confirm_transaction(&tx)
            .map_err(|e| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    format!("failed to send memo transaction: {e}"),
                )
            })?;
        let slot = self.rpc_client.get_slot().map_err(|e| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                format!("failed to get slot: {e}"),
            )
        })?;
        Ok(ProofLogReceipt {
            entry_id: signature.to_string(),
            network: self.config.cluster.clone(),
            verifier_ref: self.config.memo_program_id.clone(),
            inclusion_height: slot,
            finalized: true,
            locator: String::new(),
        })
    }

    fn ensure_finalized_commitment(&self) -> TrazaeoResult<()> {
        if self.config.commitment == "finalized" {
            Ok(())
        } else {
            Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "verification requires finalized commitment for public RPC proof logs",
            ))
        }
    }

    fn parse_transaction_proof(
        &self,
        expected_signature: &str,
        value: serde_json::Value,
    ) -> TrazaeoResult<PublicRpcTransactionProof> {
        let result = value.get("result").ok_or_else(|| {
            TrazaeoError::external("public rpc proof log adaptor", "missing transaction result")
        })?;
        let transaction = result.get("transaction").ok_or_else(|| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                "missing transaction payload",
            )
        })?;
        let signatures = transaction
            .get("signatures")
            .and_then(serde_json::Value::as_array)
            .ok_or_else(|| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    "missing transaction signatures",
                )
            })?
            .iter()
            .map(|signature| {
                signature.as_str().map(str::to_string).ok_or_else(|| {
                    TrazaeoError::serialization(
                        "public rpc proof log adaptor",
                        "transaction signature was not a string",
                    )
                })
            })
            .collect::<TrazaeoResult<Vec<_>>>()?;
        if !signatures
            .iter()
            .any(|signature| signature == expected_signature)
        {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "transaction response did not include the expected signature",
            ));
        }

        let message = transaction.get("message").ok_or_else(|| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                "missing transaction message",
            )
        })?;
        let signer_pubkeys = message
            .get("accountKeys")
            .and_then(serde_json::Value::as_array)
            .ok_or_else(|| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    "missing transaction account keys",
                )
            })?
            .iter()
            .filter(|account| {
                account
                    .get("signer")
                    .and_then(serde_json::Value::as_bool)
                    .unwrap_or(false)
            })
            .map(|account| {
                account
                    .get("pubkey")
                    .and_then(serde_json::Value::as_str)
                    .map(str::to_string)
                    .ok_or_else(|| {
                        TrazaeoError::serialization(
                            "public rpc proof log adaptor",
                            "transaction signer pubkey was not a string",
                        )
                    })
            })
            .collect::<TrazaeoResult<Vec<_>>>()?;

        let instructions = message
            .get("instructions")
            .and_then(serde_json::Value::as_array)
            .ok_or_else(|| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    "missing transaction instructions",
                )
            })?;
        for instruction in instructions {
            if instruction
                .get("programId")
                .and_then(serde_json::Value::as_str)
                == Some(self.config.memo_program_id.as_str())
            {
                let memo = self.extract_memo_payload_text(instruction)?;
                let memo_payload: PublicRpcMemoPayload =
                    serde_json::from_str(&memo).map_err(|e| {
                        TrazaeoError::serialization(
                            "public rpc proof log adaptor",
                            format!("failed to parse memo payload: {e}"),
                        )
                    })?;
                return Ok(PublicRpcTransactionProof {
                    signatures,
                    signer_pubkeys,
                    program_id: self.config.memo_program_id.clone(),
                    memo_payload,
                });
            }
        }
        Err(TrazaeoError::external(
            "public rpc proof log adaptor",
            "memo payload not found in transaction",
        ))
    }

    fn fetch_transaction_proof(&self, signature: &str) -> TrazaeoResult<PublicRpcTransactionProof> {
        let response = self
            .http_client
            .post(&self.config.rpc_url)
            .json(&json!({
                "jsonrpc": "2.0",
                "id": 1,
                "method": "getTransaction",
                "params": [
                    signature,
                    {
                        "commitment": self.config.commitment,
                        "maxSupportedTransactionVersion": 0,
                        "encoding": "jsonParsed"
                    }
                ]
            }))
            .send()
            .map_err(|e| {
                TrazaeoError::external(
                    "public rpc proof log adaptor",
                    format!("failed rpc request: {e}"),
                )
            })?;
        let value: serde_json::Value = response.json().map_err(|e| {
            TrazaeoError::serialization(
                "public rpc proof log adaptor",
                format!("failed to parse rpc response: {e}"),
            )
        })?;
        self.parse_transaction_proof(signature, value)
    }

    fn verify_payload_contains_commitment(
        &self,
        commitment: &ProofLogCommitment,
        payload: &PublicRpcMemoPayload,
    ) -> TrazaeoResult<()> {
        if payload.payload_type != "trazaeo_proof_log_commitment_v1" {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "memo payload type mismatch",
            ));
        }
        if payload.cluster != self.config.cluster
            || payload.envelope_hash != commitment.envelope_hash
            || payload.checkpoint_hash != commitment.checkpoint_hash
            || payload.log_root_hash != commitment.log_root_hash
        {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "memo payload missing expected proof-log fields",
            ));
        }
        Ok(())
    }

    fn verify_transaction_proof(
        &self,
        commitment: &ProofLogCommitment,
        proof: &PublicRpcTransactionProof,
    ) -> TrazaeoResult<()> {
        self.ensure_finalized_commitment()?;
        if proof.program_id != self.config.memo_program_id {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "transaction program mismatch",
            ));
        }
        if !proof
            .signatures
            .iter()
            .any(|signature| signature == &commitment.entry_id)
        {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "transaction signature mismatch",
            ));
        }
        if !proof
            .signer_pubkeys
            .iter()
            .any(|pubkey| pubkey == &commitment.attestor_key_ref)
        {
            return Err(TrazaeoError::external(
                "public rpc proof log adaptor",
                "proof-log attestor key did not sign the transaction",
            ));
        }
        self.verify_payload_contains_commitment(commitment, &proof.memo_payload)
    }
}

impl SolanaProofLogAdaptor {
    pub fn new(client: SolanaClient, attestor_pubkey: [u8; 32], attestor_key_ref: &str) -> Self {
        Self {
            client,
            attestor_pubkey,
            attestor_key_ref: attestor_key_ref.to_string(),
        }
    }

    pub fn chain_root(&self) -> TrazaeoResult<Hash> {
        get_chain_root(&self.client)
    }

    pub fn cluster(&self) -> String {
        cluster_name(&self.client)
    }

    pub fn program_id(&self) -> String {
        program_id(&self.client)
    }

    pub fn get_transaction(
        &self,
        signature: &str,
    ) -> TrazaeoResult<Option<crate::solana::TxResult>> {
        get_transaction(&self.client, signature)
    }

    pub fn get_proof_log_account(
        &self,
        pda: &str,
    ) -> TrazaeoResult<Option<crate::solana::AnchorAccountV1>> {
        get_anchor_account_by_pda(&self.client, pda)
    }
}

impl ProofLogAdaptor for SolanaProofLogAdaptor {
    fn log_publish_proof(
        &self,
        envelope: &PublishEnvelope,
        committed_at: &str,
        committed_unix_seconds: i64,
        prev_entry_hash: [u8; 32],
    ) -> TrazaeoResult<ProofLogPublishResult> {
        commit_publish_proof_log(PublishProofLogCommitRequest {
            envelope,
            client: &self.client,
            attestor_pubkey: self.attestor_pubkey,
            committed_at,
            committed_unix_seconds,
            prev_entry_hash,
            attestor_key_ref: &self.attestor_key_ref,
        })
    }

    fn verify_publish_proof(
        &self,
        envelope: &PublishEnvelope,
        commitment: &ProofLogCommitment,
    ) -> TrazaeoResult<()> {
        verify_proof_log_commitment_onchain(
            envelope,
            commitment,
            &self.client,
            &cluster_name(&self.client),
            &program_id(&self.client),
        )
    }
}

#[cfg(feature = "solana-proof-log-rpc")]
impl ProofLogAdaptor for PublicRpcSolanaProofLogAdaptor {
    fn log_publish_proof(
        &self,
        envelope: &PublishEnvelope,
        committed_at: &str,
        _committed_unix_seconds: i64,
        _prev_entry_hash: [u8; 32],
    ) -> TrazaeoResult<ProofLogPublishResult> {
        let memo_payload = self.build_memo_payload(envelope);
        let receipt = self.send_memo_transaction(&memo_payload)?;
        let commitment = build_proof_log_commitment(
            envelope,
            committed_at,
            &receipt.entry_id,
            &self.signer.pubkey().to_string(),
        );
        Ok(ProofLogPublishResult {
            commitment,
            receipt,
        })
    }

    fn verify_publish_proof(
        &self,
        envelope: &PublishEnvelope,
        commitment: &ProofLogCommitment,
    ) -> TrazaeoResult<()> {
        verify_proof_log_commitment_linkage(envelope, commitment).map_err(|_| {
            TrazaeoError::external(
                "public rpc proof log adaptor",
                "proof-log linkage mismatch against envelope",
            )
        })?;
        let proof = self.fetch_transaction_proof(&commitment.entry_id)?;
        self.verify_transaction_proof(commitment, &proof)
    }
}

#[cfg(all(test, feature = "solana-proof-log-rpc"))]
mod public_rpc_tests {
    use super::*;
    use serde_json::json;
    use tempfile::NamedTempFile;

    fn proof_log_commitment(signature: &str, attestor_key_ref: &str) -> ProofLogCommitment {
        ProofLogCommitment {
            entry_id: signature.to_string(),
            envelope_hash: "0909090909090909090909090909090909090909090909090909090909090909"
                .to_string(),
            checkpoint_hash: "0808080808080808080808080808080808080808080808080808080808080808"
                .to_string(),
            log_root_hash: "0707070707070707070707070707070707070707070707070707070707070707"
                .to_string(),
            committed_at: "2026-01-01T00:00:00Z".to_string(),
            attestor_key_ref: attestor_key_ref.to_string(),
        }
    }

    fn rpc_transaction_value(
        signature: &str,
        signer_pubkey: &str,
        memo_program_id: &str,
        cluster: &str,
        anchor: &ProofLogCommitment,
    ) -> serde_json::Value {
        json!({
            "result": {
                "transaction": {
                    "signatures": [signature],
                    "message": {
                        "accountKeys": [
                            {
                                "pubkey": signer_pubkey,
                                "signer": true
                            }
                        ],
                        "instructions": [
                            {
                                "programId": memo_program_id,
                                "parsed": {
                                    "memo": json!({
                                        "type": "trazaeo_proof_log_commitment_v1",
                                        "cluster": cluster,
                                        "committed_envelope_hash": anchor.envelope_hash,
                                        "committed_checkpoint_hash": anchor.checkpoint_hash,
                                        "committed_log_root_hash": anchor.log_root_hash,
                                    })
                                    .to_string()
                                }
                            }
                        ]
                    }
                }
            }
        })
    }

    fn rpc_transaction_value_with_string_parsed_memo(
        signature: &str,
        signer_pubkey: &str,
        memo_program_id: &str,
        cluster: &str,
        anchor: &ProofLogCommitment,
    ) -> serde_json::Value {
        json!({
            "result": {
                "transaction": {
                    "signatures": [signature],
                    "message": {
                        "accountKeys": [
                            {
                                "pubkey": signer_pubkey,
                                "signer": true
                            }
                        ],
                        "instructions": [
                            {
                                "program": "spl-memo",
                                "programId": memo_program_id,
                                "parsed": json!({
                                    "type": "trazaeo_proof_log_commitment_v1",
                                    "cluster": cluster,
                                    "committed_envelope_hash": anchor.envelope_hash,
                                    "committed_checkpoint_hash": anchor.checkpoint_hash,
                                    "committed_log_root_hash": anchor.log_root_hash,
                                })
                                .to_string()
                            }
                        ]
                    }
                }
            }
        })
    }

    #[test]
    fn public_rpc_verification_accepts_matching_finalized_transaction() {
        let adaptor = PublicRpcSolanaProofLogAdaptor::new("http://localhost:8899", "solana-devnet")
            .expect("create devnet adaptor");
        let anchor = proof_log_commitment("tx-1", "attestor-1");
        let proof = adaptor
            .parse_transaction_proof(
                &anchor.entry_id,
                rpc_transaction_value(
                    &anchor.entry_id,
                    &anchor.attestor_key_ref,
                    &adaptor.config.memo_program_id,
                    &adaptor.config.cluster,
                    &anchor,
                ),
            )
            .expect("parse transaction proof");

        assert!(adaptor.verify_transaction_proof(&anchor, &proof).is_ok());
    }

    #[test]
    fn public_rpc_verification_accepts_string_parsed_memo_transaction() {
        let adaptor = PublicRpcSolanaProofLogAdaptor::new("http://localhost:8899", "solana-devnet")
            .expect("create devnet adaptor");
        let anchor = proof_log_commitment("tx-1", "attestor-1");
        let proof = adaptor
            .parse_transaction_proof(
                &anchor.entry_id,
                rpc_transaction_value_with_string_parsed_memo(
                    &anchor.entry_id,
                    &anchor.attestor_key_ref,
                    &adaptor.config.memo_program_id,
                    &adaptor.config.cluster,
                    &anchor,
                ),
            )
            .expect("parse transaction proof");

        assert!(adaptor.verify_transaction_proof(&anchor, &proof).is_ok());
    }

    #[test]
    fn public_rpc_verification_rejects_non_finalized_commitment() {
        let mut adaptor =
            PublicRpcSolanaProofLogAdaptor::new("http://localhost:8899", "solana-devnet")
                .expect("create devnet adaptor");
        adaptor.config.commitment = "confirmed".to_string();
        let anchor = proof_log_commitment("tx-1", "attestor-1");
        let proof = adaptor
            .parse_transaction_proof(
                &anchor.entry_id,
                rpc_transaction_value(
                    &anchor.entry_id,
                    &anchor.attestor_key_ref,
                    &adaptor.config.memo_program_id,
                    &adaptor.config.cluster,
                    &anchor,
                ),
            )
            .expect("parse transaction proof");

        let err = adaptor
            .verify_transaction_proof(&anchor, &proof)
            .expect_err("confirmed commitment must be rejected");
        assert!(err.to_string().contains("finalized"));
    }

    #[test]
    fn public_rpc_verification_rejects_signer_mismatch() {
        let adaptor = PublicRpcSolanaProofLogAdaptor::new("http://localhost:8899", "solana-devnet")
            .expect("create devnet adaptor");
        let anchor = proof_log_commitment("tx-1", "attestor-1");
        let proof = adaptor
            .parse_transaction_proof(
                &anchor.entry_id,
                rpc_transaction_value(
                    &anchor.entry_id,
                    "different-attestor",
                    &adaptor.config.memo_program_id,
                    &adaptor.config.cluster,
                    &anchor,
                ),
            )
            .expect("parse transaction proof");

        let err = adaptor
            .verify_transaction_proof(&anchor, &proof)
            .expect_err("unexpected signer must fail verification");
        assert!(err.to_string().contains("did not sign"));
    }

    #[test]
    fn public_rpc_mainnet_requires_explicit_keypair() {
        let err =
            match PublicRpcSolanaProofLogAdaptor::new("http://localhost:8899", "solana-mainnet") {
                Ok(_) => panic!("mainnet must reject ephemeral signer"),
                Err(err) => err,
            };
        assert!(err
            .to_string()
            .contains("requires an explicit funded keypair"));
    }

    #[test]
    fn public_rpc_can_load_signer_from_keypair_path() {
        let expected_signer = Keypair::new();
        let file = NamedTempFile::new().expect("temp keypair file");
        std::fs::write(
            file.path(),
            serde_json::to_vec(&expected_signer.to_bytes().to_vec()).expect("serialize keypair"),
        )
        .expect("write keypair file");

        let adaptor = PublicRpcSolanaProofLogAdaptor::from_keypair_path(
            "http://localhost:8899",
            "solana-mainnet",
            file.path().to_str().expect("utf-8 path"),
        )
        .expect("load adaptor from keypair path");

        assert_eq!(
            adaptor.signer_pubkey(),
            expected_signer.pubkey().to_string()
        );
    }
}
