use cosmwasm_std::{
    testing::{mock_dependencies, mock_env},
    to_binary, to_vec, Binary, ContractResult,
    CosmosMsg::Stargate,
    Reply, Response, SubMsgResponse, SystemError, SystemResult, Uint128, WasmQuery,
};
use cw20::TokenInfoResponse;
use cw_token_bridge::msg::{AssetInfo, CompleteTransferResponse, TransferInfoResponse};
use ibc_translator::{
    reply::{
        contract_addr_to_base58, convert_cw20_to_bank_and_send, handle_complete_transfer_reply,
    },
    state::{CHAIN_TO_CHANNEL_MAP, CURRENT_TRANSFER, CW_DENOMS},
};
use prost::Message;
use wormhole_bindings::tokenfactory::{DenomUnit, Metadata, TokenFactoryMsg, TokenMsg};
use wormhole_sdk::Chain;

mod test_setup;
use test_setup::{default_custom_mock_deps, WORMHOLE_CONTRACT_ADDR, WORMHOLE_USER_ADDR};

#[derive(Clone, PartialEq, Message)]
struct MsgExecuteContractResponse {
    #[prost(bytes, tag = "1")]
    pub data: ::prost::alloc::vec::Vec<u8>,
}

// Tests
// 1. handle_complete_transfer_reply
//    1. happy path, GatewayTransfer
//    2. happy path, GatewayTransferWithPayload
//    2. bad msg result
//    3. invalid response data
//    4. no repsonse data
//    5. invalid reponse data type
//    6. no reponse contract
//    7. no storage
//    8. wrong stored payload
//    9. invalid recipient
//    10. invalid contract
// 2. convert_cw20_to_bank_and_send
//    1. happy path
//    2. happy path create denom
//    3. happy path create denom with empty symbol
//    4. failure invalid contract
//    5. chain id no channel
//    6. bad payload
// 3. contract_addr_to_base58
//    1. happy path
//    2. bad contract address

// TESTS: handle_complete_transfer_reply
// 1. Happy path: GatewayTransfer
#[test]
fn handle_complete_transfer_reply_happy_path() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap())
        })
    };

    let contract_addr =
        "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string();
    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    CW_DENOMS
        .save(deps.as_mut().storage, contract_addr, &tokenfactory_denom)
        .unwrap();

    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, 0, &channel)
        .unwrap();

    let transfer_payload = TransferInfoResponse {
        amount: 0u32.into(),
        token_address: [0; 32],
        token_chain: 0,
        recipient: [0; 32],
        recipient_chain: 0,
        fee: 0u32.into(),
        payload: hex::decode("7B22676174657761795F7472616E73666572223A7B22636861696E223A302C22726563697069656E74223A22633256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C22666565223A2230222C226E6F6E6365223A307D7D").unwrap()
    };
    CURRENT_TRANSFER
        .save(deps.as_mut().storage, &transfer_payload)
        .unwrap();

    // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error
    // other tests verify the correctness of this method
    // wormhole core and token bridge tests verify the correctness of the VAA parameters
    handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap();
}

// 2. Happy path: GatewayTransferWithPayload
#[test]
fn handle_complete_transfer_reply_happy_path_with_payload() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap())
        })
    };

    let contract_addr =
        "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string();
    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    CW_DENOMS
        .save(deps.as_mut().storage, contract_addr, &tokenfactory_denom)
        .unwrap();

    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, 0, &channel)
        .unwrap();

    let transfer_payload = TransferInfoResponse {
        amount: 0u32.into(),
        token_address: [0; 32],
        token_chain: 0,
        recipient: [0; 32],
        recipient_chain: 0,
        fee: 0u32.into(),
        payload: hex::decode("7B22676174657761795F7472616E736665725F776974685F7061796C6F6164223A7B22636861696E223A302C22636F6E7472616374223A22633256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C227061796C6F6164223A225647567A64464268655778765957513D222C226E6F6E6365223A307D7D").unwrap()
    };
    CURRENT_TRANSFER
        .save(deps.as_mut().storage, &transfer_payload)
        .unwrap();

    // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error
    // other tests verify the correctness of this method
    // wormhole core and token bridge tests verify the correctness of the VAA parameters
    handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap();
}

// 3. Failure: msg result is not okay
#[test]
fn handle_complete_transfer_reply_bad_msg_result() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Err("random error".to_string()),
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "msg result is not okay, we should never get here"
    );
}

// 4. Failure: could not parse reply response_data
#[test]
fn handle_complete_transfer_reply_invalid_response_data() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("eyJiYXNpY19yZWNpcGllbnQiOnsicmVjaXBpZW50IjoiYzJWcE1XVjZjelZ0WkcxM09IZDZkbU56T1haNE9YazNaR3QwY1dkbE0zbDZialIzTUd3NWJqUTAifX0=").unwrap())
        })
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "failed to parse protobuf reply response_data"
    );
}

// 5. Failure: no data in the parsed response
#[test]
fn handle_complete_transfer_reply_no_response_data() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("").unwrap()),
        }),
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "no data in the response, we should never get here"
    );
}

// 6. Failure: could not deserialize response data
#[test]
fn handle_complete_transfer_reply_invalid_response_data_type() {
    let mut deps = mock_dependencies();
    let env = mock_env();

    // encode a payload as protobuf
    // the payload is NOT a CompleteTransferResponse
    let execute_reply = MsgExecuteContractResponse {
        data: to_vec(&AssetInfo::NativeToken {
            denom: "denomA".to_string(),
        })
        .unwrap(),
    };
    let mut encoded_execute_reply = Vec::<u8>::with_capacity(execute_reply.encoded_len());
    execute_reply.encode(&mut encoded_execute_reply).unwrap();

    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(encoded_execute_reply.into()),
        }),
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(err.to_string(), "failed to deserialize response data");
}

// 7. Failure: no contract in the response
#[test]
fn handle_complete_transfer_reply_no_response_contract() {
    let mut deps = mock_dependencies();
    let env = mock_env();

    // encode a payload as protobuf
    // the payload is a CompleteTransferResponse
    let execute_reply = MsgExecuteContractResponse {
        data: to_vec(&CompleteTransferResponse {
            contract: None,
            denom: None,
            recipient: "fake".to_string(),
            amount: 1u32.into(),
            relayer: "fake".to_string(),
            fee: 0u32.into(),
        })
        .unwrap(),
    };
    let mut encoded_execute_reply = Vec::<u8>::with_capacity(execute_reply.encoded_len());
    execute_reply.encode(&mut encoded_execute_reply).unwrap();

    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(encoded_execute_reply.into()),
        }),
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "no contract in response, we should never get here"
    );
}

// 8. Failure: no current transfer in storage
#[test]
fn handle_complete_transfer_reply_no_storage() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap())
        })
    };

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "failed to load current transfer from storage"
    );
}

// 9. Failure: could not deserialize payload3 payload from stored transfer
#[test]
fn handle_complete_transfer_reply_wrong_stored_payload() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap())
        })
    };

    let bad_transfer_payload = TransferInfoResponse {
        amount: 0u32.into(),
        token_address: [0; 32],
        token_chain: 0,
        recipient: [0; 32],
        recipient_chain: 0,
        fee: 0u32.into(),
        payload: hex::decode("7b22726563697069656e74223a7b22726563697069656e74223a22633256704d575636637a56745a4731334f486436646d4e7a4f585a344f586b335a4774306357646c4d336c36626a52334d477735626a5130227d7d").unwrap()
    };
    CURRENT_TRANSFER
        .save(deps.as_mut().storage, &bad_transfer_payload)
        .unwrap();

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(err.to_string(), "failed to deserialize transfer payload");
}

// 10. Failure: could not convert the recipient (GatewayTransfer) base64 encoded bytes to a utf8 string
#[test]
fn handle_complete_transfer_reply_invalid_recipient() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("CvgBeyJjb250cmFjdCI6InNlaTE0MG02eGFnbXcwemVzZWp6aHN2azQ2enByZ3Njcjd0dTk0aDM2cndzdXRjc3hjczRmbWRzOXNldnltIiwiZGVub20iOm51bGwsInJlY2lwaWVudCI6InNlaTFka2R3ZHZrbngwcWF2NWNwNWt3Njhta24zcjk5bTNzdmt5amZ2a3p0d2g5N2R2MmxtMGtzajZ4cmFrIiwiYW1vdW50IjoiMTAwMCIsInJlbGF5ZXIiOiJzZWkxdmhrbTJxdjc4NHJ1bHg4eWxydTB6cHZ5dnczbTNjeTl4M3h5ZnYiLCJmZWUiOiIwIn0=").unwrap())
        })
    };

    let bad_transfer_payload = TransferInfoResponse {
        amount: 0u32.into(),
        token_address: [0; 32],
        token_chain: 0,
        recipient: [0; 32],
        recipient_chain: 0,
        fee: 0u32.into(),
        payload: hex::decode("7B22676174657761795F7472616E73666572223A7B22636861696E223A302C22726563697069656E74223A223256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C22666565223A2230222C226E6F6E6365223A307D7D").unwrap()
    };
    CURRENT_TRANSFER
        .save(deps.as_mut().storage, &bad_transfer_payload)
        .unwrap();

    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string"
    );
}

// 11. Failure: could not convert the contract (GatewayTransferWithPayload) base64 encoded bytes to a utf8 string
#[test]
fn handle_complete_transfer_reply_invalid_contract() {
    let mut deps = mock_dependencies();
    let env = mock_env();
    let msg = Reply {
        id: 1,
        result: cosmwasm_std::SubMsgResult::Ok(SubMsgResponse {
            events: vec![],
            data: Some(Binary::from_base64("Cv0BeyJjb250cmFjdCI6Indvcm1ob2xlMXl3NHd2MnpxZzl4a242N3p2cTNhenllMHQ4aDB4OWtneWczZDUzanltMjRneHQ0OXZkeXM2czhoN2EiLCJkZW5vbSI6bnVsbCwicmVjaXBpZW50Ijoic2VpMWRrZHdkdmtueDBxYXY1Y3A1a3c2OG1rbjNyOTltM3N2a3lqZnZrenR3aDk3ZHYybG0wa3NqNnhyYWsiLCJhbW91bnQiOiIxMDAwIiwicmVsYXllciI6InNlaTF2aGttMnF2Nzg0cnVseDh5bHJ1MHpwdnl2dzNtM2N5OXgzeHlmdiIsImZlZSI6IjAifQ==").unwrap())
        })
    };

    let bad_transfer_payload = TransferInfoResponse {
        amount: 0u32.into(),
        token_address: [0; 32],
        token_chain: 0,
        recipient: [0; 32],
        recipient_chain: 0,
        fee: 0u32.into(),
        payload: hex::decode("7B22676174657761795F7472616E736665725F776974685F7061796C6F6164223A7B22636861696E223A302C22636F6E7472616374223A223256704D575636637A56745A4731334F486436646D4E7A4F585A344F586B335A4774306357646C4D336C36626A52334D477735626A5130222C227061796C6F6164223A225647567A64464268655778765957513D222C226E6F6E6365223A307D7D").unwrap()
    };
    CURRENT_TRANSFER
        .save(deps.as_mut().storage, &bad_transfer_payload)
        .unwrap();

    // just verifying that we called the convert_cw20_to_bank -- unwrap the result without an error
    // other tests verify the correctness of this method
    // wormhole core and token bridge tests verify the correctness of the VAA parameters
    let err = handle_complete_transfer_reply(deps.as_mut(), env, msg).unwrap_err();
    assert_eq!(
        err.to_string(),
        "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string"
    );
}

// Test convert_cw20_to_bank_and_send
// TESTS: convert_cw20_to_bank
// 1. Happy path
#[test]
fn convert_cw20_to_bank_and_send_happy_path() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string();

    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    CW_DENOMS
        .save(
            deps.as_mut().storage,
            contract_addr.clone(),
            &tokenfactory_denom,
        )
        .unwrap();

    let chain_id = Chain::Ethereum;
    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, chain_id.into(), &channel)
        .unwrap();

    let response = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        None,
    )
    .unwrap();

    // response should have 2 messages:
    assert_eq!(response.messages.len(), 2);

    let mut expected_response: Response<TokenFactoryMsg> = Response::new();
    expected_response = expected_response.add_message(TokenMsg::MintTokens {
        denom: tokenfactory_denom,
        amount,
        mint_to_address: "cosmos2contract".to_string(),
    });
    expected_response = expected_response.add_message(Stargate {
        type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(),
        value: Binary::from_base64("Cgh0cmFuc2ZlchIJY2hhbm5lbC0wGkkKRGZhY3RvcnkvY29zbW9zMmNvbnRyYWN0LzNRRVF5aTdpeUpId1E0d2ZVTUxGUEI0a1J6Y3pNQVhDaXRXaDdoNlRFVERhEgExIg9jb3Ntb3MyY29udHJhY3QqL3dvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwOL2irKOC6IugFg==").unwrap(),
    });

    // 1. TokenMsg::MintTokens
    assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,);

    // 2. Stargate ibc transfer
    assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,);
}

// 2. Happy path + CreateDenom on TokenFactory
#[test]
fn convert_cw20_to_bank_happy_path_create_denom() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string();

    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    let subdenom = "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();

    let chain_id = Chain::Ethereum;
    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, chain_id.into(), &channel)
        .unwrap();

    let token_info_response = TokenInfoResponse {
        name: "TestCoin".to_string(),
        symbol: "TEST".to_string(),
        decimals: 6,
        total_supply: Uint128::new(10_000_000),
    };
    let token_info_response_copy = token_info_response.clone();
    deps.querier.update_wasm(move |q| match q {
        WasmQuery::Smart {
            contract_addr: _,
            msg: _,
        } => SystemResult::Ok(ContractResult::Ok(
            to_binary(&token_info_response_copy).unwrap(),
        )),
        _ => SystemResult::Err(SystemError::UnsupportedRequest {
            kind: "wasm".to_string(),
        }),
    });

    // Populate token factory token's metadata from cw20 token's metadata
    let tf_description = token_info_response.name.clone()
        + ", "
        + token_info_response.symbol.as_str()
        + ", "
        + tokenfactory_denom.as_str();
    let tf_denom_unit_base = DenomUnit {
        denom: tokenfactory_denom.clone(),
        exponent: 0,
        aliases: vec![],
    };
    let tf_scaled_denom = "wormhole/".to_string()
        + subdenom.as_str()
        + "/"
        + token_info_response.decimals.to_string().as_str();
    let tf_denom_unit_scaled = DenomUnit {
        denom: tf_scaled_denom.clone(),
        exponent: u32::from(token_info_response.decimals),
        aliases: vec![],
    };
    let tf_metadata = Metadata {
        description: Some(tf_description),
        base: Some(tokenfactory_denom.clone()),
        denom_units: vec![tf_denom_unit_base, tf_denom_unit_scaled],
        display: Some(tf_scaled_denom),
        name: Some(token_info_response.name),
        symbol: Some(token_info_response.symbol),
    };

    let response = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        None,
    )
    .unwrap();

    // response should have 3 messages:
    assert_eq!(response.messages.len(), 3);

    let mut expected_response: Response<TokenFactoryMsg> = Response::new();
    expected_response = expected_response.add_message(TokenMsg::CreateDenom {
        subdenom,
        metadata: Some(tf_metadata),
    });
    expected_response = expected_response.add_message(TokenMsg::MintTokens {
        denom: tokenfactory_denom,
        amount,
        mint_to_address: "cosmos2contract".to_string(),
    });
    expected_response = expected_response.add_message(Stargate {
        type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(),
        value: Binary::from_base64("Cgh0cmFuc2ZlchIJY2hhbm5lbC0wGkkKRGZhY3RvcnkvY29zbW9zMmNvbnRyYWN0LzNRRVF5aTdpeUpId1E0d2ZVTUxGUEI0a1J6Y3pNQVhDaXRXaDdoNlRFVERhEgExIg9jb3Ntb3MyY29udHJhY3QqL3dvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwOL2irKOC6IugFg==").unwrap(),
    });

    // 1. TokenMsg::CreateDenom
    assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,);

    // 2. TokenMsg::MintTokens
    assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,);

    // 3. Stargate ibc transfer
    assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,);
}

// 3. Happy path + CreateDenom on TokenFactory with empty symbol
#[test]
fn convert_cw20_to_bank_happy_path_create_denom_empty_symbol() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string();

    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    let subdenom = "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();

    let chain_id = Chain::Ethereum;
    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, chain_id.into(), &channel)
        .unwrap();

    let token_info_response = TokenInfoResponse {
        name: "TestCoin".to_string(),
        symbol: "".to_string(),
        decimals: 6,
        total_supply: Uint128::new(10_000_000),
    };
    let token_info_response_copy = token_info_response.clone();
    deps.querier.update_wasm(move |q| match q {
        WasmQuery::Smart {
            contract_addr: _,
            msg: _,
        } => SystemResult::Ok(ContractResult::Ok(
            to_binary(&token_info_response_copy).unwrap(),
        )),
        _ => SystemResult::Err(SystemError::UnsupportedRequest {
            kind: "wasm".to_string(),
        }),
    });

    // Populate token factory token's metadata from cw20 token's metadata
    let tf_denom_unit_base = DenomUnit {
        denom: tokenfactory_denom.clone(),
        exponent: 0,
        aliases: vec![],
    };
    let tf_scaled_denom = "wormhole/".to_string()
        + subdenom.as_str()
        + "/"
        + token_info_response.decimals.to_string().as_str();
    let tf_denom_unit_scaled = DenomUnit {
        denom: tf_scaled_denom.clone(),
        exponent: u32::from(token_info_response.decimals),
        aliases: vec![],
    };
    let tf_description = token_info_response.name.clone()
        + ", "
        + tf_scaled_denom.as_str() // CW20 symbol is empty, use tf_scaled_denom
        + ", "
        + tokenfactory_denom.as_str();
    let tf_metadata = Metadata {
        description: Some(tf_description),
        base: Some(tokenfactory_denom.clone()),
        denom_units: vec![tf_denom_unit_base, tf_denom_unit_scaled],
        display: Some(tf_scaled_denom.clone()),
        name: Some(token_info_response.name),
        symbol: Some(tf_scaled_denom), // CW20 symbol is empty, use tf_scaled_denom
    };

    let response = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        None,
    )
    .unwrap();

    // response should have 3 messages:
    assert_eq!(response.messages.len(), 3);

    let mut expected_response: Response<TokenFactoryMsg> = Response::new();
    expected_response = expected_response.add_message(TokenMsg::CreateDenom {
        subdenom,
        metadata: Some(tf_metadata),
    });
    expected_response = expected_response.add_message(TokenMsg::MintTokens {
        denom: tokenfactory_denom,
        amount,
        mint_to_address: "cosmos2contract".to_string(),
    });
    expected_response = expected_response.add_message(Stargate {
        type_url: "/ibc.applications.transfer.v1.MsgTransfer".to_string(),
        value: Binary::from_base64("Cgh0cmFuc2ZlchIJY2hhbm5lbC0wGkkKRGZhY3RvcnkvY29zbW9zMmNvbnRyYWN0LzNRRVF5aTdpeUpId1E0d2ZVTUxGUEI0a1J6Y3pNQVhDaXRXaDdoNlRFVERhEgExIg9jb3Ntb3MyY29udHJhY3QqL3dvcm1ob2xlMXZoa20ycXY3ODRydWx4OHlscnUwenB2eXZ3M20zY3k5OWU2d3kwOL2irKOC6IugFg==").unwrap(),
    });

    // 1. TokenMsg::CreateDenom
    assert_eq!(response.messages[0].msg, expected_response.messages[0].msg,);

    // 2. TokenMsg::MintTokens
    assert_eq!(response.messages[1].msg, expected_response.messages[1].msg,);

    // 3. Stargate ibc transfer
    assert_eq!(response.messages[2].msg, expected_response.messages[2].msg,);
}

// 4. Failure: couldn't validate contract address
#[test]
fn convert_cw20_to_bank_failure_invalid_contract() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = "badContractAddr".to_string();
    let chain_id = Chain::Ethereum;

    let method_err = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        None,
    )
    .unwrap_err();
    assert_eq!(
        method_err.to_string(),
        "invalid contract address badContractAddr"
    );
}

// 5. Failure: Chain id doesn't have a channel
#[test]
fn convert_cw20_to_bank_and_send_chain_id_no_channel() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string();

    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    CW_DENOMS
        .save(
            deps.as_mut().storage,
            contract_addr.clone(),
            &tokenfactory_denom,
        )
        .unwrap();

    let chain_id = Chain::Ethereum;

    let method_err = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        None,
    )
    .unwrap_err();

    assert_eq!(
        method_err.to_string(),
        "chain id does not have an allowed channel"
    );
}

// 6. Failure: bad payload
#[test]
fn convert_cw20_to_bank_and_send_bad_payload() {
    let mut deps = default_custom_mock_deps();
    let env = mock_env();
    let recipient = WORMHOLE_USER_ADDR.to_string();
    let amount = 1;
    let contract_addr = WORMHOLE_CONTRACT_ADDR.to_string();

    let tokenfactory_denom =
        "factory/cosmos2contract/3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa".to_string();
    CW_DENOMS
        .save(
            deps.as_mut().storage,
            contract_addr.clone(),
            &tokenfactory_denom,
        )
        .unwrap();

    let chain_id = Chain::Ethereum;
    let channel = "channel-0".to_string();
    CHAIN_TO_CHANNEL_MAP
        .save(deps.as_mut().storage, chain_id.into(), &channel)
        .unwrap();

    let method_err = convert_cw20_to_bank_and_send(
        deps.as_mut(),
        env,
        recipient,
        amount,
        contract_addr,
        chain_id.into(),
        Some(
            Binary::from_base64("2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0").unwrap(),
        ),
    )
    .unwrap_err();

    assert_eq!(
        method_err.to_string(),
        "failed to convert 2VpMWV6czVtZG13OHd6dmNzOXZ4OXk3ZGt0cWdlM3l6bjR3MGw5bjQ0= to utf8 string"
    );
}

// 1. happy path
#[test]
fn contract_addr_to_base58_happy_path() {
    let deps = default_custom_mock_deps();
    let b58_str = contract_addr_to_base58(
        deps.as_ref(),
        "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7a".to_string(),
    )
    .unwrap();
    assert_eq!(b58_str, "3QEQyi7iyJHwQ4wfUMLFPB4kRzczMAXCitWh7h6TETDa");
}

// 2. bad contract address, could not canonicalize contract address
#[test]
fn contract_addr_to_base58_bad_contract_address() {
    let deps = default_custom_mock_deps();
    let method_err = contract_addr_to_base58(
        deps.as_ref(),
        "wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7".to_string(),
    )
    .unwrap_err();
    assert_eq!(
        method_err.to_string(),
        "could not canonicalize contract address wormhole1yw4wv2zqg9xkn67zvq3azye0t8h0x9kgyg3d53jym24gxt49vdys6s8h7"
    )
}
