use {
    crate::cli_output::CliSignatureVerificationStatus,
    base64::{prelude::BASE64_STANDARD, Engine},
    chrono::{Local, NaiveDateTime, SecondsFormat, TimeZone, Utc},
    console::style,
    indicatif::{ProgressBar, ProgressStyle},
    solana_cli_config::SettingType,
    solana_sdk::{
        clock::UnixTimestamp,
        hash::Hash,
        instruction::CompiledInstruction,
        message::v0::MessageAddressTableLookup,
        native_token::lamports_to_sol,
        program_utils::limited_deserialize,
        pubkey::Pubkey,
        signature::Signature,
        stake,
        transaction::{TransactionError, TransactionVersion, VersionedTransaction},
    },
    solana_transaction_status::{
        Rewards, UiReturnDataEncoding, UiTransactionReturnData, UiTransactionStatusMeta,
    },
    spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id},
    std::{collections::HashMap, fmt, io, time::Duration},
};

#[derive(Clone, Debug)]
pub struct BuildBalanceMessageConfig {
    pub use_lamports_unit: bool,
    pub show_unit: bool,
    pub trim_trailing_zeros: bool,
}

impl Default for BuildBalanceMessageConfig {
    fn default() -> Self {
        Self {
            use_lamports_unit: false,
            show_unit: true,
            trim_trailing_zeros: true,
        }
    }
}

fn is_memo_program(k: &Pubkey) -> bool {
    let k_str = k.to_string();
    (k_str == spl_memo_v1_id().to_string()) || (k_str == spl_memo_id().to_string())
}

pub fn build_balance_message_with_config(
    lamports: u64,
    config: &BuildBalanceMessageConfig,
) -> String {
    let value = if config.use_lamports_unit {
        lamports.to_string()
    } else {
        let sol = lamports_to_sol(lamports);
        let sol_str = format!("{sol:.9}");
        if config.trim_trailing_zeros {
            sol_str
                .trim_end_matches('0')
                .trim_end_matches('.')
                .to_string()
        } else {
            sol_str
        }
    };
    let unit = if config.show_unit {
        if config.use_lamports_unit {
            let ess = if lamports == 1 { "" } else { "s" };
            format!(" lamport{ess}")
        } else {
            " SOL".to_string()
        }
    } else {
        "".to_string()
    };
    format!("{value}{unit}")
}

pub fn build_balance_message(lamports: u64, use_lamports_unit: bool, show_unit: bool) -> String {
    build_balance_message_with_config(
        lamports,
        &BuildBalanceMessageConfig {
            use_lamports_unit,
            show_unit,
            ..BuildBalanceMessageConfig::default()
        },
    )
}

// Pretty print a "name value"
pub fn println_name_value(name: &str, value: &str) {
    let styled_value = if value.is_empty() {
        style("(not set)").italic()
    } else {
        style(value)
    };
    println!("{} {}", style(name).bold(), styled_value);
}

pub fn writeln_name_value(f: &mut dyn fmt::Write, name: &str, value: &str) -> fmt::Result {
    let styled_value = if value.is_empty() {
        style("(not set)").italic()
    } else {
        style(value)
    };
    writeln!(f, "{} {}", style(name).bold(), styled_value)
}

pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
    let description = match setting_type {
        SettingType::Explicit => "",
        SettingType::Computed => "(computed)",
        SettingType::SystemDefault => "(default)",
    };

    println!(
        "{} {} {}",
        style(name).bold(),
        style(value),
        style(description).italic(),
    );
}

pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap<String, String>) -> String {
    let label = address_labels.get(pubkey);
    match label {
        Some(label) => format!(
            "{:.31} ({:.4}..{})",
            label,
            pubkey,
            pubkey.split_at(pubkey.len() - 4).1
        ),
        None => pubkey.to_string(),
    }
}

pub fn println_signers(
    blockhash: &Hash,
    signers: &[String],
    absent: &[String],
    bad_sig: &[String],
) {
    println!();
    println!("Blockhash: {blockhash}");
    if !signers.is_empty() {
        println!("Signers (Pubkey=Signature):");
        signers.iter().for_each(|signer| println!("  {signer}"))
    }
    if !absent.is_empty() {
        println!("Absent Signers (Pubkey):");
        absent.iter().for_each(|pubkey| println!("  {pubkey}"))
    }
    if !bad_sig.is_empty() {
        println!("Bad Signatures (Pubkey):");
        bad_sig.iter().for_each(|pubkey| println!("  {pubkey}"))
    }
    println!();
}

struct CliAccountMeta {
    is_signer: bool,
    is_writable: bool,
    is_invoked: bool,
}

fn format_account_mode(meta: CliAccountMeta) -> String {
    format!(
        "{}r{}{}", // accounts are always readable...
        if meta.is_signer {
            "s" // stands for signer
        } else {
            "-"
        },
        if meta.is_writable {
            "w" // comment for consistent rust fmt (no joking; lol)
        } else {
            "-"
        },
        // account may be executable on-chain while not being
        // designated as a program-id in the message
        if meta.is_invoked {
            "x"
        } else {
            // programs to be executed via CPI cannot be identified as
            // executable from the message
            "-"
        },
    )
}

fn write_transaction<W: io::Write>(
    w: &mut W,
    transaction: &VersionedTransaction,
    transaction_status: Option<&UiTransactionStatusMeta>,
    prefix: &str,
    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
    block_time: Option<UnixTimestamp>,
    timezone: CliTimezone,
) -> io::Result<()> {
    write_block_time(w, block_time, timezone, prefix)?;

    let message = &transaction.message;
    let account_keys: Vec<AccountKeyType> = {
        let static_keys_iter = message
            .static_account_keys()
            .iter()
            .map(AccountKeyType::Known);
        let dynamic_keys: Vec<AccountKeyType> = message
            .address_table_lookups()
            .map(transform_lookups_to_unknown_keys)
            .unwrap_or_default();
        static_keys_iter.chain(dynamic_keys).collect()
    };

    write_version(w, transaction.version(), prefix)?;
    write_recent_blockhash(w, message.recent_blockhash(), prefix)?;
    write_signatures(w, &transaction.signatures, sigverify_status, prefix)?;

    let mut fee_payer_index = None;
    for (account_index, account) in account_keys.iter().enumerate() {
        if fee_payer_index.is_none() && message.is_non_loader_key(account_index) {
            fee_payer_index = Some(account_index)
        }

        let account_meta = CliAccountMeta {
            is_signer: message.is_signer(account_index),
            is_writable: message.is_maybe_writable(account_index),
            is_invoked: message.is_invoked(account_index),
        };

        write_account(
            w,
            account_index,
            *account,
            format_account_mode(account_meta),
            Some(account_index) == fee_payer_index,
            prefix,
        )?;
    }

    for (instruction_index, instruction) in message.instructions().iter().enumerate() {
        let program_pubkey = account_keys[instruction.program_id_index as usize];
        let instruction_accounts = instruction
            .accounts
            .iter()
            .map(|account_index| (account_keys[*account_index as usize], *account_index));

        write_instruction(
            w,
            instruction_index,
            program_pubkey,
            instruction,
            instruction_accounts,
            prefix,
        )?;
    }

    if let Some(address_table_lookups) = message.address_table_lookups() {
        write_address_table_lookups(w, address_table_lookups, prefix)?;
    }

    if let Some(transaction_status) = transaction_status {
        write_status(w, &transaction_status.status, prefix)?;
        write_fees(w, transaction_status.fee, prefix)?;
        write_balances(w, transaction_status, prefix)?;
        write_compute_units_consumed(
            w,
            transaction_status.compute_units_consumed.clone().into(),
            prefix,
        )?;
        write_log_messages(w, transaction_status.log_messages.as_ref().into(), prefix)?;
        write_return_data(w, transaction_status.return_data.as_ref().into(), prefix)?;
        write_rewards(w, transaction_status.rewards.as_ref().into(), prefix)?;
    } else {
        writeln!(w, "{prefix}Status: Unavailable")?;
    }

    Ok(())
}

fn transform_lookups_to_unknown_keys(lookups: &[MessageAddressTableLookup]) -> Vec<AccountKeyType> {
    let unknown_writable_keys = lookups
        .iter()
        .enumerate()
        .flat_map(|(lookup_index, lookup)| {
            lookup
                .writable_indexes
                .iter()
                .map(move |table_index| AccountKeyType::Unknown {
                    lookup_index,
                    table_index: *table_index,
                })
        });

    let unknown_readonly_keys = lookups
        .iter()
        .enumerate()
        .flat_map(|(lookup_index, lookup)| {
            lookup
                .readonly_indexes
                .iter()
                .map(move |table_index| AccountKeyType::Unknown {
                    lookup_index,
                    table_index: *table_index,
                })
        });

    unknown_writable_keys.chain(unknown_readonly_keys).collect()
}

enum CliTimezone {
    Local,
    #[allow(dead_code)]
    Utc,
}

fn write_block_time<W: io::Write>(
    w: &mut W,
    block_time: Option<UnixTimestamp>,
    timezone: CliTimezone,
    prefix: &str,
) -> io::Result<()> {
    if let Some(block_time) = block_time {
        let block_time_output = match timezone {
            CliTimezone::Local => format!("{:?}", Local.timestamp_opt(block_time, 0).unwrap()),
            CliTimezone::Utc => format!("{:?}", Utc.timestamp_opt(block_time, 0).unwrap()),
        };
        writeln!(w, "{prefix}Block Time: {block_time_output}",)?;
    }
    Ok(())
}

fn write_version<W: io::Write>(
    w: &mut W,
    version: TransactionVersion,
    prefix: &str,
) -> io::Result<()> {
    let version = match version {
        TransactionVersion::Legacy(_) => "legacy".to_string(),
        TransactionVersion::Number(number) => number.to_string(),
    };
    writeln!(w, "{prefix}Version: {version}")
}

fn write_recent_blockhash<W: io::Write>(
    w: &mut W,
    recent_blockhash: &Hash,
    prefix: &str,
) -> io::Result<()> {
    writeln!(w, "{prefix}Recent Blockhash: {recent_blockhash:?}")
}

fn write_signatures<W: io::Write>(
    w: &mut W,
    signatures: &[Signature],
    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
    prefix: &str,
) -> io::Result<()> {
    let sigverify_statuses = if let Some(sigverify_status) = sigverify_status {
        sigverify_status.iter().map(|s| format!(" ({s})")).collect()
    } else {
        vec!["".to_string(); signatures.len()]
    };
    for (signature_index, (signature, sigverify_status)) in
        signatures.iter().zip(&sigverify_statuses).enumerate()
    {
        writeln!(
            w,
            "{prefix}Signature {signature_index}: {signature:?}{sigverify_status}",
        )?;
    }
    Ok(())
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AccountKeyType<'a> {
    Known(&'a Pubkey),
    Unknown {
        lookup_index: usize,
        table_index: u8,
    },
}

impl fmt::Display for AccountKeyType<'_> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Known(address) => write!(f, "{address}"),
            Self::Unknown {
                lookup_index,
                table_index,
            } => {
                write!(
                    f,
                    "Unknown Address (uses lookup {lookup_index} and index {table_index})"
                )
            }
        }
    }
}

fn write_account<W: io::Write>(
    w: &mut W,
    account_index: usize,
    account_address: AccountKeyType,
    account_mode: String,
    is_fee_payer: bool,
    prefix: &str,
) -> io::Result<()> {
    writeln!(
        w,
        "{}Account {}: {} {}{}",
        prefix,
        account_index,
        account_mode,
        account_address,
        if is_fee_payer { " (fee payer)" } else { "" },
    )
}

fn write_instruction<'a, W: io::Write>(
    w: &mut W,
    instruction_index: usize,
    program_pubkey: AccountKeyType,
    instruction: &CompiledInstruction,
    instruction_accounts: impl Iterator<Item = (AccountKeyType<'a>, u8)>,
    prefix: &str,
) -> io::Result<()> {
    writeln!(w, "{prefix}Instruction {instruction_index}")?;
    writeln!(
        w,
        "{}  Program:   {} ({})",
        prefix, program_pubkey, instruction.program_id_index
    )?;
    for (index, (account_address, account_index)) in instruction_accounts.enumerate() {
        writeln!(
            w,
            "{prefix}  Account {index}: {account_address} ({account_index})"
        )?;
    }

    let mut raw = true;
    if let AccountKeyType::Known(program_pubkey) = program_pubkey {
        if program_pubkey == &solana_vote_program::id() {
            if let Ok(vote_instruction) = limited_deserialize::<
                solana_vote_program::vote_instruction::VoteInstruction,
            >(&instruction.data)
            {
                writeln!(w, "{prefix}  {vote_instruction:?}")?;
                raw = false;
            }
        } else if program_pubkey == &stake::program::id() {
            if let Ok(stake_instruction) =
                limited_deserialize::<stake::instruction::StakeInstruction>(&instruction.data)
            {
                writeln!(w, "{prefix}  {stake_instruction:?}")?;
                raw = false;
            }
        } else if program_pubkey == &solana_sdk::system_program::id() {
            if let Ok(system_instruction) = limited_deserialize::<
                solana_sdk::system_instruction::SystemInstruction,
            >(&instruction.data)
            {
                writeln!(w, "{prefix}  {system_instruction:?}")?;
                raw = false;
            }
        } else if is_memo_program(program_pubkey) {
            if let Ok(s) = std::str::from_utf8(&instruction.data) {
                writeln!(w, "{prefix}  Data: \"{s}\"")?;
                raw = false;
            }
        }
    }

    if raw {
        writeln!(w, "{}  Data: {:?}", prefix, instruction.data)?;
    }

    Ok(())
}

fn write_address_table_lookups<W: io::Write>(
    w: &mut W,
    address_table_lookups: &[MessageAddressTableLookup],
    prefix: &str,
) -> io::Result<()> {
    for (lookup_index, lookup) in address_table_lookups.iter().enumerate() {
        writeln!(w, "{prefix}Address Table Lookup {lookup_index}",)?;
        writeln!(w, "{}  Table Account: {}", prefix, lookup.account_key,)?;
        writeln!(
            w,
            "{}  Writable Indexes: {:?}",
            prefix,
            &lookup.writable_indexes[..],
        )?;
        writeln!(
            w,
            "{}  Readonly Indexes: {:?}",
            prefix,
            &lookup.readonly_indexes[..],
        )?;
    }
    Ok(())
}

fn write_rewards<W: io::Write>(
    w: &mut W,
    rewards: Option<&Rewards>,
    prefix: &str,
) -> io::Result<()> {
    if let Some(rewards) = rewards {
        if !rewards.is_empty() {
            writeln!(w, "{prefix}Rewards:",)?;
            writeln!(
                w,
                "{}  {:<44}  {:^15}  {:<16}  {:<20}",
                prefix, "Address", "Type", "Amount", "New Balance"
            )?;
            for reward in rewards {
                let sign = if reward.lamports < 0 { "-" } else { "" };
                writeln!(
                    w,
                    "{}  {:<44}  {:^15}  {}◎{:<14.9}  ◎{:<18.9}",
                    prefix,
                    reward.pubkey,
                    if let Some(reward_type) = reward.reward_type {
                        format!("{reward_type}")
                    } else {
                        "-".to_string()
                    },
                    sign,
                    lamports_to_sol(reward.lamports.unsigned_abs()),
                    lamports_to_sol(reward.post_balance)
                )?;
            }
        }
    }
    Ok(())
}

fn write_status<W: io::Write>(
    w: &mut W,
    transaction_status: &Result<(), TransactionError>,
    prefix: &str,
) -> io::Result<()> {
    writeln!(
        w,
        "{}Status: {}",
        prefix,
        match transaction_status {
            Ok(_) => "Ok".into(),
            Err(err) => err.to_string(),
        }
    )
}

fn write_fees<W: io::Write>(w: &mut W, transaction_fee: u64, prefix: &str) -> io::Result<()> {
    writeln!(w, "{}  Fee: ◎{}", prefix, lamports_to_sol(transaction_fee))
}

fn write_balances<W: io::Write>(
    w: &mut W,
    transaction_status: &UiTransactionStatusMeta,
    prefix: &str,
) -> io::Result<()> {
    assert_eq!(
        transaction_status.pre_balances.len(),
        transaction_status.post_balances.len()
    );
    for (i, (pre, post)) in transaction_status
        .pre_balances
        .iter()
        .zip(transaction_status.post_balances.iter())
        .enumerate()
    {
        if pre == post {
            writeln!(
                w,
                "{}  Account {} balance: ◎{}",
                prefix,
                i,
                lamports_to_sol(*pre)
            )?;
        } else {
            writeln!(
                w,
                "{}  Account {} balance: ◎{} -> ◎{}",
                prefix,
                i,
                lamports_to_sol(*pre),
                lamports_to_sol(*post)
            )?;
        }
    }
    Ok(())
}

fn write_return_data<W: io::Write>(
    w: &mut W,
    return_data: Option<&UiTransactionReturnData>,
    prefix: &str,
) -> io::Result<()> {
    if let Some(return_data) = return_data {
        let (data, encoding) = &return_data.data;
        let raw_return_data = match encoding {
            UiReturnDataEncoding::Base64 => BASE64_STANDARD.decode(data).map_err(|err| {
                io::Error::new(
                    io::ErrorKind::Other,
                    format!("could not parse data as {encoding:?}: {err:?}"),
                )
            })?,
        };
        if !raw_return_data.is_empty() {
            use pretty_hex::*;
            writeln!(
                w,
                "{}Return Data from Program {}:",
                prefix, return_data.program_id
            )?;
            writeln!(w, "{}  {:?}", prefix, raw_return_data.hex_dump())?;
        }
    }
    Ok(())
}

fn write_compute_units_consumed<W: io::Write>(
    w: &mut W,
    compute_units_consumed: Option<u64>,
    prefix: &str,
) -> io::Result<()> {
    if let Some(cus) = compute_units_consumed {
        writeln!(w, "{prefix}Compute Units Consumed: {cus}")?;
    }
    Ok(())
}

fn write_log_messages<W: io::Write>(
    w: &mut W,
    log_messages: Option<&Vec<String>>,
    prefix: &str,
) -> io::Result<()> {
    if let Some(log_messages) = log_messages {
        if !log_messages.is_empty() {
            writeln!(w, "{prefix}Log Messages:",)?;
            for log_message in log_messages {
                writeln!(w, "{prefix}  {log_message}")?;
            }
        }
    }
    Ok(())
}

pub fn println_transaction(
    transaction: &VersionedTransaction,
    transaction_status: Option<&UiTransactionStatusMeta>,
    prefix: &str,
    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
    block_time: Option<UnixTimestamp>,
) {
    let mut w = Vec::new();
    if write_transaction(
        &mut w,
        transaction,
        transaction_status,
        prefix,
        sigverify_status,
        block_time,
        CliTimezone::Local,
    )
    .is_ok()
    {
        if let Ok(s) = String::from_utf8(w) {
            print!("{s}");
        }
    }
}

pub fn writeln_transaction(
    f: &mut dyn fmt::Write,
    transaction: &VersionedTransaction,
    transaction_status: Option<&UiTransactionStatusMeta>,
    prefix: &str,
    sigverify_status: Option<&[CliSignatureVerificationStatus]>,
    block_time: Option<UnixTimestamp>,
) -> fmt::Result {
    let mut w = Vec::new();
    let write_result = write_transaction(
        &mut w,
        transaction,
        transaction_status,
        prefix,
        sigverify_status,
        block_time,
        CliTimezone::Local,
    );

    if write_result.is_ok() {
        if let Ok(s) = String::from_utf8(w) {
            write!(f, "{s}")?;
        }
    }
    Ok(())
}

/// Creates a new process bar for processing that will take an unknown amount of time
pub fn new_spinner_progress_bar() -> ProgressBar {
    let progress_bar = ProgressBar::new(42);
    progress_bar.set_style(
        ProgressStyle::default_spinner()
            .template("{spinner:.green} {wide_msg}")
            .expect("ProgresStyle::template direct input to be correct"),
    );
    progress_bar.enable_steady_tick(Duration::from_millis(100));
    progress_bar
}

pub fn unix_timestamp_to_string(unix_timestamp: UnixTimestamp) -> String {
    match NaiveDateTime::from_timestamp_opt(unix_timestamp, 0) {
        Some(ndt) => Utc
            .from_utc_datetime(&ndt)
            .to_rfc3339_opts(SecondsFormat::Secs, true),
        None => format!("UnixTimestamp {unix_timestamp}"),
    }
}

#[cfg(test)]
mod test {
    use {
        super::*,
        solana_sdk::{
            message::{
                v0::{self, LoadedAddresses},
                Message as LegacyMessage, MessageHeader, VersionedMessage,
            },
            pubkey::Pubkey,
            signature::{Keypair, Signer},
            transaction::Transaction,
            transaction_context::TransactionReturnData,
        },
        solana_transaction_status::{Reward, RewardType, TransactionStatusMeta},
        std::io::BufWriter,
    };

    fn new_test_keypair() -> Keypair {
        let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap();
        let public = ed25519_dalek::PublicKey::from(&secret);
        let keypair = ed25519_dalek::Keypair { secret, public };
        Keypair::from_bytes(&keypair.to_bytes()).unwrap()
    }

    fn new_test_v0_transaction() -> VersionedTransaction {
        let keypair = new_test_keypair();
        let account_key = Pubkey::new_from_array([1u8; 32]);
        let address_table_key = Pubkey::new_from_array([2u8; 32]);
        VersionedTransaction::try_new(
            VersionedMessage::V0(v0::Message {
                header: MessageHeader {
                    num_required_signatures: 1,
                    num_readonly_signed_accounts: 0,
                    num_readonly_unsigned_accounts: 1,
                },
                recent_blockhash: Hash::default(),
                account_keys: vec![keypair.pubkey(), account_key],
                address_table_lookups: vec![MessageAddressTableLookup {
                    account_key: address_table_key,
                    writable_indexes: vec![0],
                    readonly_indexes: vec![1],
                }],
                instructions: vec![CompiledInstruction::new_from_raw_parts(
                    3,
                    vec![],
                    vec![1, 2],
                )],
            }),
            &[&keypair],
        )
        .unwrap()
    }

    #[test]
    fn test_write_legacy_transaction() {
        let keypair = new_test_keypair();
        let account_key = Pubkey::new_from_array([1u8; 32]);
        let transaction = VersionedTransaction::from(Transaction::new(
            &[&keypair],
            LegacyMessage {
                header: MessageHeader {
                    num_required_signatures: 1,
                    num_readonly_signed_accounts: 0,
                    num_readonly_unsigned_accounts: 1,
                },
                recent_blockhash: Hash::default(),
                account_keys: vec![keypair.pubkey(), account_key],
                instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])],
            },
            Hash::default(),
        ));

        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction);
        let meta = TransactionStatusMeta {
            status: Ok(()),
            fee: 5000,
            pre_balances: vec![5000, 10_000],
            post_balances: vec![0, 9_900],
            inner_instructions: None,
            log_messages: Some(vec!["Test message".to_string()]),
            pre_token_balances: None,
            post_token_balances: None,
            rewards: Some(vec![Reward {
                pubkey: account_key.to_string(),
                lamports: -100,
                post_balance: 9_900,
                reward_type: Some(RewardType::Rent),
                commission: None,
            }]),
            loaded_addresses: LoadedAddresses::default(),
            return_data: Some(TransactionReturnData {
                program_id: Pubkey::new_from_array([2u8; 32]),
                data: vec![1, 2, 3],
            }),
            compute_units_consumed: Some(1234u64),
        };

        let output = {
            let mut write_buffer = BufWriter::new(Vec::new());
            write_transaction(
                &mut write_buffer,
                &transaction,
                Some(&meta.into()),
                "",
                Some(&sigverify_status),
                Some(1628633791),
                CliTimezone::Utc,
            )
            .unwrap();
            let bytes = write_buffer.into_inner().unwrap();
            String::from_utf8(bytes).unwrap()
        };

        assert_eq!(
            output,
            r"Block Time: 2021-08-10T22:16:31Z
Version: legacy
Recent Blockhash: 11111111111111111111111111111111
Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass)
Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
Account 1: -r-x 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
Instruction 0
  Program:   4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
  Account 0: 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (0)
  Data: []
Status: Ok
  Fee: ◎0.000005
  Account 0 balance: ◎0.000005 -> ◎0
  Account 1 balance: ◎0.00001 -> ◎0.0000099
Compute Units Consumed: 1234
Log Messages:
  Test message
Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
  Length: 3 (0x3) bytes
0000:   01 02 03                                             ...
Rewards:
  Address                                            Type        Amount            New Balance         \0
  4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi        rent        -◎0.000000100     ◎0.000009900       \0
".replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
        );
    }

    #[test]
    fn test_write_v0_transaction() {
        let versioned_tx = new_test_v0_transaction();
        let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&versioned_tx);
        let address_table_entry1 = Pubkey::new_from_array([3u8; 32]);
        let address_table_entry2 = Pubkey::new_from_array([4u8; 32]);
        let loaded_addresses = LoadedAddresses {
            writable: vec![address_table_entry1],
            readonly: vec![address_table_entry2],
        };
        let meta = TransactionStatusMeta {
            status: Ok(()),
            fee: 5000,
            pre_balances: vec![5000, 10_000, 15_000, 20_000],
            post_balances: vec![0, 10_000, 14_900, 20_000],
            inner_instructions: None,
            log_messages: Some(vec!["Test message".to_string()]),
            pre_token_balances: None,
            post_token_balances: None,
            rewards: Some(vec![Reward {
                pubkey: address_table_entry1.to_string(),
                lamports: -100,
                post_balance: 14_900,
                reward_type: Some(RewardType::Rent),
                commission: None,
            }]),
            loaded_addresses,
            return_data: Some(TransactionReturnData {
                program_id: Pubkey::new_from_array([2u8; 32]),
                data: vec![1, 2, 3],
            }),
            compute_units_consumed: Some(2345u64),
        };

        let output = {
            let mut write_buffer = BufWriter::new(Vec::new());
            write_transaction(
                &mut write_buffer,
                &versioned_tx,
                Some(&meta.into()),
                "",
                Some(&sigverify_status),
                Some(1628633791),
                CliTimezone::Utc,
            )
            .unwrap();
            let bytes = write_buffer.into_inner().unwrap();
            String::from_utf8(bytes).unwrap()
        };

        assert_eq!(
            output,
            r"Block Time: 2021-08-10T22:16:31Z
Version: 0
Recent Blockhash: 11111111111111111111111111111111
Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass)
Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer)
Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi
Account 2: -rw- Unknown Address (uses lookup 0 and index 0)
Account 3: -r-x Unknown Address (uses lookup 0 and index 1)
Instruction 0
  Program:   Unknown Address (uses lookup 0 and index 1) (3)
  Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1)
  Account 1: Unknown Address (uses lookup 0 and index 0) (2)
  Data: []
Address Table Lookup 0
  Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR
  Writable Indexes: [0]
  Readonly Indexes: [1]
Status: Ok
  Fee: ◎0.000005
  Account 0 balance: ◎0.000005 -> ◎0
  Account 1 balance: ◎0.00001
  Account 2 balance: ◎0.000015 -> ◎0.0000149
  Account 3 balance: ◎0.00002
Compute Units Consumed: 2345
Log Messages:
  Test message
Return Data from Program 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR:
  Length: 3 (0x3) bytes
0000:   01 02 03                                             ...
Rewards:
  Address                                            Type        Amount            New Balance         \0
  CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8        rent        -◎0.000000100     ◎0.000014900       \0
".replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI
        );
    }

    #[test]
    fn test_format_labeled_address() {
        let pubkey = Pubkey::default().to_string();
        let mut address_labels = HashMap::new();

        assert_eq!(format_labeled_address(&pubkey, &address_labels), pubkey);

        address_labels.insert(pubkey.to_string(), "Default Address".to_string());
        assert_eq!(
            &format_labeled_address(&pubkey, &address_labels),
            "Default Address (1111..1111)"
        );

        address_labels.insert(
            pubkey.to_string(),
            "abcdefghijklmnopqrstuvwxyz1234567890".to_string(),
        );
        assert_eq!(
            &format_labeled_address(&pubkey, &address_labels),
            "abcdefghijklmnopqrstuvwxyz12345 (1111..1111)"
        );
    }
}
