//! Contains various tests for checking the `forge create` subcommand

use crate::{
    constants::*,
    utils::{self, EnvExternalities},
};
use alloy_primitives::{Address, hex};
use anvil::{NodeConfig, spawn};
use foundry_compilers::artifacts::{BytecodeHash, remappings::Remapping};
use foundry_test_utils::{
    forgetest, forgetest_async,
    snapbox::IntoData,
    str,
    util::{OutputExt, TestCommand, TestProject},
};
use std::{fs, str::FromStr};

/// This will insert _dummy_ contract that uses a library
///
/// **NOTE** This is intended to be linked against a random address and won't actually work. The
/// purpose of this is _only_ to make sure we can deploy contracts linked against addresses.
///
/// This will create a library `remapping/MyLib.sol:MyLib`
///
/// returns the contract argument for the create command
fn setup_with_simple_remapping(prj: &TestProject) -> String {
    // explicitly set remapping and libraries
    prj.update_config(|config| {
        config.remappings = vec![Remapping::from_str("remapping/=lib/remapping/").unwrap().into()];
        config.libraries = vec![format!("remapping/MyLib.sol:MyLib:{:?}", Address::random())];
    });

    prj.add_source(
        "LinkTest",
        r#"
import "remapping/MyLib.sol";
contract LinkTest {
    function foo() public returns (uint256) {
        return MyLib.foobar(1);
    }
}
"#,
    );

    prj.add_lib(
        "remapping/MyLib",
        r"
library MyLib {
    function foobar(uint256 a) public view returns (uint256) {
    	return a * 100;
    }
}
",
    );

    "src/LinkTest.sol:LinkTest".to_string()
}

fn setup_oracle(prj: &TestProject) -> String {
    prj.update_config(|c| {
        c.libraries = vec![format!(
            "./src/libraries/ChainlinkTWAP.sol:ChainlinkTWAP:{:?}",
            Address::random()
        )];
    });

    prj.add_source(
        "Contract",
        r#"
import {ChainlinkTWAP} from "./libraries/ChainlinkTWAP.sol";
contract Contract {
    function getPrice() public view returns (int latest) {
        latest = ChainlinkTWAP.getLatestPrice(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE);
    }
}
"#,
    );

    prj.add_source(
        "libraries/ChainlinkTWAP",
        r"
library ChainlinkTWAP {
   function getLatestPrice(address base) public view returns (int256) {
        return 0;
   }
}
",
    );

    "src/Contract.sol:Contract".to_string()
}

forgetest!(create_rejects_unsupported_remote_sponsor, |_prj, cmd| {
    cmd.args([
        "create",
        "src/Counter.sol:Counter",
        "--sponsor-url",
        "https://sponsor.tempo.xyz/tp_test",
    ])
    .assert_failure()
    .stderr_eq(str![[r#"
Error: --sponsor-url is not supported by forge create; use --tempo.sponsor with --tempo.sponsor-signer or --tempo.sponsor-sig

"#]]);
});

/// configures the `TestProject` with the given closure and calls the `forge create` command
fn create_on_chain<F>(info: Option<EnvExternalities>, prj: TestProject, mut cmd: TestCommand, f: F)
where
    F: FnOnce(&TestProject) -> String,
{
    if let Some(info) = info {
        let contract_path = f(&prj);

        let output = cmd
            .arg("create")
            .args(info.create_args())
            .arg(contract_path)
            .assert_success()
            .get_output()
            .stdout_lossy();
        let _address = utils::parse_deployed_address(output.as_str())
            .unwrap_or_else(|| panic!("Failed to parse deployer {output}"));
    }
}

// tests `forge` create on goerli if correct env vars are set
forgetest!(can_create_simple_on_goerli, |prj, cmd| {
    create_on_chain(EnvExternalities::goerli(), prj, cmd, setup_with_simple_remapping);
});

// tests `forge` create on goerli if correct env vars are set
forgetest!(can_create_oracle_on_goerli, |prj, cmd| {
    create_on_chain(EnvExternalities::goerli(), prj, cmd, setup_oracle);
});

// tests `forge` create on amoy if correct env vars are set
forgetest!(can_create_oracle_on_amoy, |prj, cmd| {
    create_on_chain(EnvExternalities::amoy(), prj, cmd, setup_oracle);
});

// tests that we can deploy the template contract
forgetest_async!(can_create_template_contract, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // explicitly byte code hash for consistent checks
    prj.update_config(|c| c.bytecode_hash = BytecodeHash::None);

    // Dry-run without the `--broadcast` flag
    cmd.forge_fuse().args([
        "create",
        format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
        "--rpc-url",
        rpc.as_str(),
        "--private-key",
        pk.as_str(),
    ]);

    // Dry-run
    cmd.assert().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Contract: Counter
Transaction: {
  "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
  "to": null,
  "maxFeePerGas": "0x77359401",
  "maxPriorityFeePerGas": "0x1",
  "gas": "0x241e7",
  "input": "[..]",
  "nonce": "0x0",
  "chainId": "0x7a69"
}
ABI: [
  {
    "type": "function",
    "name": "increment",
    "inputs": [],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "type": "function",
    "name": "number",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "setNumber",
    "inputs": [
      {
        "name": "newNumber",
        "type": "uint256",
        "internalType": "uint256"
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  }
]


"#]]);

    // Dry-run with `--json` flag
    cmd.arg("--json").assert().stdout_eq(
        str![[r#"
{
  "contract": "Counter",
  "transaction": {
    "from": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
    "to": null,
    "maxFeePerGas": "0x77359401",
    "maxPriorityFeePerGas": "0x1",
    "gas": "0x241e7",
    "input": "[..]",
    "nonce": "0x0",
    "chainId": "0x7a69"
  },
  "abi": [
    {
      "type": "function",
      "name": "increment",
      "inputs": [],
      "outputs": [],
      "stateMutability": "nonpayable"
    },
    {
      "type": "function",
      "name": "number",
      "inputs": [],
      "outputs": [
        {
          "name": "",
          "type": "uint256",
          "internalType": "uint256"
        }
      ],
      "stateMutability": "view"
    },
    {
      "type": "function",
      "name": "setNumber",
      "inputs": [
        {
          "name": "newNumber",
          "type": "uint256",
          "internalType": "uint256"
        }
      ],
      "outputs": [],
      "stateMutability": "nonpayable"
    }
  ]
}

"#]]
        .is_json(),
    );

    cmd.forge_fuse().args([
        "create",
        format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
        "--rpc-url",
        rpc.as_str(),
        "--private-key",
        pk.as_str(),
        "--broadcast",
    ]);

    cmd.assert().stdout_eq(str![[r#"
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[TX_HASH]

"#]]);
});

forgetest_async!(create_rejects_invalid_eip1559_fees_before_access_list, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    let stderr = cmd
        .forge_fuse()
        .args([
            "create",
            format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            pk.as_str(),
            "--access-list",
            "--gas-price",
            "1",
            "--priority-gas-price",
            "2",
        ])
        .assert_failure()
        .get_output()
        .stderr_lossy();

    assert!(
        stderr.contains("Error: max priority fee per gas (2) cannot exceed max fee per gas (1)"),
        "{stderr}"
    );
});

forgetest_async!(create_resolves_tempo_expires_before_broadcast, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test_tempo()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // explicitly byte code hash for consistent checks
    prj.update_config(|c| c.bytecode_hash = BytecodeHash::None);

    let assert = cmd
        .forge_fuse()
        .args([
            "create",
            format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--tempo.expires",
            "30",
        ])
        .assert_success();
    let output = assert.get_output();
    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);

    assert!(
        stderr.contains("Transaction expires at unix timestamp "),
        "expected create to print resolved tempo expiry, got:\n{stderr}",
    );
    assert!(stdout.contains("Deployed to:"), "{stdout}");
});

forgetest_async!(create_broadcasts_with_local_tempo_sponsor, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test_tempo()).await;
    let rpc = handle.http_endpoint();
    let wallets = handle.dev_wallets().take(2).collect::<Vec<_>>();
    let sender_key = hex::encode(wallets[0].credential().to_bytes());
    let sponsor_key =
        format!("private-key://0x{}", hex::encode(wallets[1].credential().to_bytes()));
    let sponsor = format!("{:?}", wallets[1].address());

    prj.update_config(|config| config.bytecode_hash = BytecodeHash::None);

    let assert = cmd
        .forge_fuse()
        .args([
            "create",
            format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
            "--rpc-url",
            &rpc,
            "--private-key",
            &sender_key,
            "--broadcast",
            "--tempo.fee-token",
            "PathUSD",
            "--tempo.sponsor",
            &sponsor,
            "--tempo.sponsor-signer",
            &sponsor_key,
        ])
        .assert_success();
    let output = assert.get_output();

    let stdout = String::from_utf8_lossy(&output.stdout);
    let stderr = String::from_utf8_lossy(&output.stderr);
    assert!(stdout.contains("Deployed to:"), "{stdout}");
    assert!(stderr.to_ascii_lowercase().contains(&format!("tempo sponsor: {sponsor}")), "{stderr}");
});

forgetest_async!(create_rejects_tempo_access_key_before_broadcast, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test_tempo()).await;
    let rpc = handle.http_endpoint();

    prj.update_config(|config| config.bytecode_hash = BytecodeHash::None);
    let stderr = cmd
        .forge_fuse()
        .args([
            "create",
            format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
            "--rpc-url",
            &rpc,
            "--tempo.access-key",
            "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
            "--tempo.root-account",
            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
            "--broadcast",
        ])
        .assert_failure()
        .get_output()
        .stderr_lossy();

    assert!(stderr.contains("Tempo access-key transactions cannot use CREATE"), "{stderr}");
});

// tests that we can deploy the template contract
forgetest_async!(can_create_using_unlocked, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());
    prj.initialize_default_contracts();

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let dev = handle.dev_accounts().next().unwrap();

    // explicitly byte code hash for consistent checks
    prj.update_config(|c| c.bytecode_hash = BytecodeHash::None);

    // A matching Tempo Accounts entry must not change an ordinary Ethereum deployment into a
    // Tempo transaction.
    let tempo_home = tempfile::tempdir().unwrap();
    let wallet_dir = tempo_home.path().join("wallet");
    fs::create_dir_all(&wallet_dir).unwrap();
    let store = serde_json::json!({
        "tempo-cli.store": {
            "state": {
                "activeAccount": 0,
                "chainId": 31337,
                "accounts": [{"address": format!("{dev:?}")}],
                "accessKeys": [{
                    "access": format!("{dev:?}"),
                    "address": "0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
                    "chainId": 31337,
                    "keyType": "secp256k1",
                    "privateKey": "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
                }],
            },
        },
    });
    fs::write(wallet_dir.join("store.json"), serde_json::to_vec(&store).unwrap()).unwrap();

    cmd.forge_fuse();
    cmd.env("TEMPO_HOME", tempo_home.path());
    cmd.args([
        "create",
        format!("./src/{TEMPLATE_CONTRACT}.sol:{TEMPLATE_CONTRACT}").as_str(),
        "--rpc-url",
        rpc.as_str(),
        "--from",
        format!("{dev:?}").as_str(),
        "--unlocked",
        "--broadcast",
    ]);

    cmd.assert().stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[TX_HASH]

"#]]);

    cmd.assert().stdout_eq(str![[r#"
No files changed, compilation skipped
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
[TX_HASH]

"#]]);
});

// tests that we can deploy with constructor args
forgetest_async!(can_create_with_constructor_args, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // explicitly byte code hash for consistent checks
    prj.update_config(|c| c.bytecode_hash = BytecodeHash::None);

    prj.add_source(
        "ConstructorContract",
        r#"
contract ConstructorContract {
    string public name;

    constructor(string memory _name) {
        name = _name;
    }
}
"#,
    );

    cmd.forge_fuse()
        .args([
            "create",
            "./src/ConstructorContract.sol:ConstructorContract",
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--constructor-args",
            "My Constructor",
        ])
        .assert_success()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[TX_HASH]

"#]]);

    prj.add_source(
        "TupleArrayConstructorContract",
        r#"
struct Point {
    uint256 x;
    uint256 y;
}

contract TupleArrayConstructorContract {
    constructor(Point[] memory _points) {}
}
"#,
    );

    cmd.forge_fuse()
        .args([
            "create",
            "./src/TupleArrayConstructorContract.sol:TupleArrayConstructorContract",
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
            "--constructor-args",
            "[(1,2), (2,3), (3,4)]",
        ])
        .assert()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful!
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
[TX_HASH]

"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/6332>
forgetest_async!(can_create_and_call, |prj, cmd| {
    foundry_test_utils::util::initialize(prj.root());

    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    // explicitly byte code hash for consistent checks
    prj.update_config(|c| c.bytecode_hash = BytecodeHash::None);

    prj.add_source(
        "UniswapV2Swap",
        r#"
contract UniswapV2Swap {

    function pairInfo() public view returns (uint reserveA, uint reserveB, uint totalSupply) {
       (reserveA, reserveB, totalSupply) = (0,0,0);
    }

}
"#,
    );

    cmd.forge_fuse()
        .args([
            "create",
            "./src/UniswapV2Swap.sol:UniswapV2Swap",
            "--rpc-url",
            rpc.as_str(),
            "--private-key",
            pk.as_str(),
            "--broadcast",
        ])
        .assert_success()
        .stdout_eq(str![[r#"
[COMPILING_FILES] with [SOLC_VERSION]
[SOLC_VERSION] [ELAPSED]
Compiler run successful with warnings:
Warning (2018): Function state mutability can be restricted to pure
 [FILE]:6:5:
  |
6 |     function pairInfo() public view returns (uint reserveA, uint reserveB, uint totalSupply) {
  |     ^ (Relevant source part starts here and spans across multiple lines).

Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3
[TX_HASH]

"#]]);
});

// <https://github.com/foundry-rs/foundry/issues/10156>
forgetest_async!(should_err_if_no_bytecode, |prj, cmd| {
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();

    prj.add_source(
        "AbstractCounter.sol",
        r#"
abstract contract AbstractCounter {
    uint256 public number;

    function setNumberV1(uint256 newNumber) public {
        number = newNumber;
    }

    function incrementV1() public {
        number++;
    }
}
    "#,
    );

    cmd.args([
        "create",
        "./src/AbstractCounter.sol:AbstractCounter",
        "--rpc-url",
        rpc.as_str(),
        "--private-key",
        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
        "--broadcast",
    ])
    .assert_failure()
    .stderr_eq(str![[r#"
Error: no bytecode found in bin object for AbstractCounter

"#]]);
});

// Tests that `forge create` fails when the deployment transaction reverts
// <https://github.com/foundry-rs/foundry/issues/13954>
forgetest_async!(flaky_should_fail_on_reverted_deployment, |prj, cmd| {
    let (_api, handle) = spawn(NodeConfig::test()).await;
    let rpc = handle.http_endpoint();
    let wallet = handle.dev_wallets().next().unwrap();
    let pk = hex::encode(wallet.credential().to_bytes());

    prj.add_source(
        "RevertingContract.sol",
        r#"
contract RevertingContract {
    constructor() {
        revert("deployment failed");
    }
}
    "#,
    );

    // Use --gas-limit to bypass eth_estimateGas, which would reject the tx early.
    // This simulates chains that mine reverted txs (e.g. when gas is manually specified).
    cmd.args([
        "create",
        "./src/RevertingContract.sol:RevertingContract",
        "--rpc-url",
        rpc.as_str(),
        "--private-key",
        pk.as_str(),
        "--broadcast",
        "--gas-limit",
        "1000000",
    ])
    .assert_failure()
    .stderr_eq(str![[r#"
Error: deployment transaction failed (receipt status 0): [..]

"#]]);
});
