mod session;
use chisel::session::ChiselSession as CachedChiselSession;
use foundry_evm::core::evm::EthEvmNetwork;
use session::ChiselSession;
use std::{
    path::PathBuf,
    time::{SystemTime, UNIX_EPOCH},
};

struct CacheCleanup(Vec<String>);

impl Drop for CacheCleanup {
    fn drop(&mut self) {
        for id in &self.0 {
            let _ = CachedChiselSession::<EthEvmNetwork>::remove_cached_session(id);
        }
    }
}

fn unique_cache_id(prefix: &str) -> String {
    let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
    format!("{prefix}-{}-{timestamp}", std::process::id())
}

fn cache_file(id: &str) -> PathBuf {
    PathBuf::from(format!(
        "{}chisel-{id}.json",
        CachedChiselSession::<EthEvmNetwork>::cache_dir().unwrap()
    ))
}

macro_rules! repl_test {
    ($name:ident, | $cmd:ident | $test:expr) => {
        repl_test!($name, "", |$cmd| $test);
    };
    ($name:ident, $flags:expr, | $cmd:ident | $test:expr) => {
        repl_test!($name, $flags, init = false, |$cmd| $test);
    };
    ($name:ident, $flags:expr,init = $init:expr, | $cmd:ident | $test:expr) => {
        #[test]
        #[allow(unused_mut)]
        fn $name() {
            let mut $cmd = ChiselSession::new(stringify!($name), $flags, $init);
            $test;
            return (); // Fix "go to definition" due to `tokio::test`.
        }
    };
}

repl_test!(repl_help, |repl| {
    repl.sendln_raw("!h");
    repl.expect("Chisel help");
    repl.expect_prompt();
});

repl_test!(save_renamed_session_removes_stale_cache, |repl| {
    let old_id = unique_cache_id("rename-old");
    let new_id = unique_cache_id("rename-new");
    let _cleanup = CacheCleanup(vec![old_id.clone(), new_id.clone()]);

    repl.sendln(&format!("!save {old_id}"));
    repl.sendln(&format!("!save {new_id}"));
    repl.sendln_raw("!list");
    repl.expect(&format!("chisel-{new_id}.json"));
    repl.expect_prompt();

    // A renamed session must not leave the previous cache entry loadable.
    repl.sendln_raw(&format!("!load {old_id}"));
    repl.expect("failed to load session");
    repl.expect_prompt();

    repl.sendln_raw(&format!("!load {new_id}"));
    repl.expect(&format!("Loaded Chisel session! (ID = {new_id})"));
    repl.expect_prompt();
});

repl_test!(save_case_only_rename_preserves_destination, |repl| {
    let old_id = unique_cache_id("rename-case").to_ascii_lowercase();
    let new_id = old_id.to_ascii_uppercase();
    let _cleanup = CacheCleanup(vec![old_id.clone(), new_id.clone()]);

    repl.sendln(&format!("!save {old_id}"));
    let old_cache_file = cache_file(&old_id);
    let new_cache_file = cache_file(&new_id);
    let paths_alias =
        std::fs::canonicalize(&old_cache_file).ok() == std::fs::canonicalize(&new_cache_file).ok();

    repl.sendln(&format!("!save {new_id}"));

    // On case-insensitive filesystems, both IDs resolve to the same cache path.
    repl.sendln_raw(&format!("!load {new_id}"));
    repl.expect(&format!("Loaded Chisel session! (ID = {new_id})"));
    repl.expect_prompt();

    if !paths_alias {
        repl.sendln_raw(&format!("!load {old_id}"));
        repl.expect("failed to load session");
        repl.expect_prompt();
    }
});

repl_test!(failed_save_restores_previous_session_id, |repl| {
    let first_id = unique_cache_id("failed-save-first");
    let second_id = unique_cache_id("failed-save-second");
    let _cleanup = CacheCleanup(vec![first_id.clone(), second_id.clone()]);
    let invalid_id = format!("{}/id", unique_cache_id("failed-save-invalid"));

    repl.sendln(&format!("!save {first_id}"));
    // The nested path makes the write fail without touching the existing cache file.
    repl.sendln_raw(&format!("!save {invalid_id}"));
    repl.expect("No such file or directory");
    repl.expect_prompt();

    // A failed rename must not lose the ID of the last successfully saved file.
    repl.sendln(&format!("!save {second_id}"));
    repl.sendln_raw("!list");
    repl.expect(&format!("chisel-{second_id}.json"));
    repl.expect_prompt();

    repl.sendln_raw(&format!("!load {first_id}"));
    repl.expect("failed to load session");
    repl.expect_prompt();
});

// Test abi encode/decode.
repl_test!(abi_encode_decode, |repl| {
    repl.sendln("bytes memory encoded = abi.encode(42, \"hello\")");
    repl.sendln("(uint num, string memory str) = abi.decode(encoded, (uint, string))");
    repl.sendln("num");
    repl.expect("42");
    repl.sendln("str");
    repl.expect("hello");
});

// Issue #5253: Dynamic bytes should be displayed as their raw value.
repl_test!(dynamic_bytes_display, |repl| {
    repl.sendln(r#"bytes memory encoded = abi.encode("Not initialized")"#);
    repl.sendln("bytes memory decoded = abi.decode(encoded, (bytes))");
    repl.sendln(r#"bytes memory raw = bytes("Not initialized")"#);

    repl.sendln("encoded");
    repl.expect(
        "Data: 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000f4e6f7420696e697469616c697a65640000000000000000000000000000000000",
    );

    repl.sendln("decoded");
    repl.expect("Data: 0x4e6f7420696e697469616c697a6564");

    repl.sendln("raw");
    repl.expect("Data: 0x4e6f7420696e697469616c697a6564");
});

// Test 0x prefixed strings.
repl_test!(hex_string_interpretation, |repl| {
    repl.sendln("string memory s = \"0x1234\"");
    repl.sendln("s");
    // Should be treated as string, not hex literal.
    repl.expect("0x1234");
});

// Hex literals in the generated inspector `abi.encode(...)` call are ABI-encoded as dynamic bytes.
repl_test!(hex_literal_inspection_type, |repl| {
    repl.sendln("hex\"6869\"");
    repl.expect("Type: dynamic bytes");
    repl.sendln("(hex\"6869\")");
    repl.expect("Type: dynamic bytes");
});

// Test cheatcodes availability.
repl_test!(cheatcodes_available, "", init = true, |repl| {
    repl.sendln("address alice = address(0x1)");

    repl.sendln("alice.balance");
    repl.expect("Decimal: 0");

    repl.sendln("vm.deal();");
    repl.expect("Wrong argument count for function call");

    repl.sendln("vm.deal(alice, 1 ether);");

    repl.sendln("alice.balance");
    repl.expect("Decimal: 1000000000000000000");
});

// Test empty inputs.
repl_test!(empty_input, |repl| {
    repl.sendln("   \n \n\n    \t \t \n \n\t\t\t\t \n \n");
});

// Issue #4130: Test type(intN).min correctness.
repl_test!(int_min_values, |repl| {
    repl.sendln("type(int8).min");
    repl.expect("-128");
    repl.sendln("type(int256).min");
    repl.expect("-57896044618658097711785492504343953926634992332820282019728792003956564819968");
});

// Issue #4393: Test edit command with traces.
// TODO: test `!edit`
// repl_test!(edit_with_traces, |repl| {
//     repl.sendln("!traces");
//     repl.sendln("uint x = 42");
//     repl.sendln("!edit");
//     // Should open editor without errors.
//     repl.expect("Running");
// });

// Test tuple support.
repl_test!(tuples, |repl| {
    repl.sendln("(uint a, uint b) = (1, 2)");
    repl.sendln("a");
    repl.expect("Decimal: 1");
    repl.sendln("b");
    repl.expect("Decimal: 2");
});

// Issue #4467: Test import.
repl_test!(import, "", init = true, |repl| {
    repl.sendln("import {Counter} from \"src/Counter.sol\"");
    repl.sendln("Counter c = new Counter()");
    // TODO: pre-existing inspection failure.
    // repl.sendln("c.number()");
    repl.sendln("uint x = c.number();\nx");
    repl.expect("Decimal: 0");
    repl.sendln("c.increment();");
    // repl.sendln("c.number()");
    repl.sendln("x = c.number();\nx");
    repl.expect("Decimal: 1");
});

// Issue #4617: Test code after assembly return.
repl_test!(assembly_return, |repl| {
    repl.sendln("uint x = 1;");
    repl.sendln("assembly { mstore(0x0, 0x1337) return(0x0, 0x20) }");
    repl.sendln("x = 2;");
    repl.sendln("!md");
    // Should work without errors.
    repl.expect("[0x00:0x20]: 0x0000000000000000000000000000000000000000000000000000000000001337");
});

// Issue #4652: Test commands with trailing whitespace.
repl_test!(trailing_whitespace, |repl| {
    repl.sendln("uint x = 42   ");
    repl.sendln("x");
    repl.expect("Decimal: 42");
});

// Issue #4652: Test that solc flags are respected.
repl_test!(solc_flags, "--use 0.8.23", |repl| {
    repl.sendln("pragma solidity 0.8.24;");
    repl.expect("invalid compiler version");
});

// Issue #4915: `chisel eval`
repl_test!(eval_subcommand, "eval type(uint8).max", |repl| {
    repl.expect("Decimal: 255");
});

repl_test!(
    eval_tempo_network_uses_tempo_executor,
    "--network tempo eval address(0xfeEC000000000000000000000000000000000000).code.length",
    |repl| {
        repl.expect("Decimal: 1");
    }
);

repl_test!(
    eval_tempo_chain_id_uses_tempo_executor,
    "--chain 4217 eval address(0xfeEC000000000000000000000000000000000000).code.length",
    |repl| {
        repl.expect("Decimal: 1");
    }
);

repl_test!(
    eval_tempo_named_chain_uses_tempo_executor,
    "--chain tempo eval address(0xfeEC000000000000000000000000000000000000).code.length",
    |repl| {
        repl.expect("Decimal: 1");
    }
);

// Issue #4938: Test memory/stack dumps with assembly.
repl_test!(assembly_memory_dump, |repl| {
    let input = r#"
uint256 value = 12345;
string memory str;
assembly {
    str := add(mload(0x40), 0x80)
    mstore(0x40, add(str, 0x20))
    mstore(str, 0)
    let end := str
}
"#;
    repl.sendln_raw(input.trim());
    repl.expect_prompts(3);
    repl.sendln("value");
    repl.expect("Decimal: 12345");
    repl.sendln("!md");
    repl.expect("[0x00:0x20]");
});

// Assembly as the final statement with a return — exercises the path where both
// `first_yul_return_span` and `trailing_assembly_last_stmt_span` resolve to the same `return(...)`
// span (no subsequent Solidity statement after the assembly block).
repl_test!(assembly_return_final, |repl| {
    repl.sendln("uint x = 0xbeef;");
    repl.sendln("assembly { mstore(0x0, sload(0)) return(0x0, 0x20) }");
    repl.sendln("!md");
    repl.expect("[0x00:0x20]");
});

// Assembly block without a `return(...)` call as an intermediate statement, exercises
// `first_yul_return_span` returning `None` while a subsequent Solidity statement is still evaluated
// correctly.
repl_test!(assembly_no_return_intermediate, |repl| {
    repl.sendln("uint x = 1;");
    repl.sendln("assembly { x := add(x, 1) }");
    repl.sendln("x");
    repl.expect("Decimal: 2");
});

// Issue #5051, #8978: Test EVM version normalization.
repl_test!(flaky_evm_version_normalization, "--use 0.7.6 --evm-version london", |repl| {
    repl.sendln("uint x;\nx");
    repl.expect("Decimal: 0");
});

// Issue #5481: Test function return values are displayed.
repl_test!(function_return_display, |repl| {
    repl.sendln("function add(uint a, uint b) public pure returns (uint) { return a + b; }");
    repl.sendln("add(2, 3)");
    repl.expect("Decimal: 5");
});

// Issue #5737: Test bytesN return types.
repl_test!(bytes_length_type, |repl| {
    repl.sendln("bytes10 b = bytes10(0)");
    repl.sendln("b.length");
    repl.expect("Decimal: 10");
});

// Issue #5737: Test bytesN indexing return type.
repl_test!(bytes_index_type, |repl| {
    repl.sendln("bytes32 b = bytes32(uint256(0x0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20))");
    repl.sendln("b[3]");
    repl.expect("Data: 0x0400000000000000000000000000000000000000000000000000000000000000");
});

// Issue #6618: Test fetching interface with structs.
repl_test!(fetch_interface_with_structs, |repl| {
    repl.sendln_raw("!fe 0x5ff137d4b0fdcd49dca30c7cf57e578a026d2789 IEntryPoint");
    repl.expect(
        "Added 0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789's interface to source as `IEntryPoint`",
    );
    repl.expect_prompt();
    repl.sendln("uint256 x = 1;\nx");
    repl.expect("Decimal: 1");
});

// Issue #7035: Test that hex strings aren't checksummed as addresses.
repl_test!(hex_string_no_checksum, |repl| {
    repl.sendln("function test(string memory s) public pure returns (string memory) { return s; }");
    repl.sendln("test(\"0xe5f3af50fe5d0bf402a3c6f55ccc47d4307922d4\")");
    // Should return the exact string, not checksummed.
    repl.expect("0xe5f3af50fe5d0bf402a3c6f55ccc47d4307922d4");
});

// Issue #7050: Test enum min/max operations.
repl_test!(enum_min_max, |repl| {
    repl.sendln("enum Color { Red, Green, Blue }");
    repl.sendln("type(Color).min");
    repl.expect("Decimal: 0");
    repl.sendln("type(Color).max");
    repl.expect("Decimal: 2");
});

// Issue #7193: Test that inspected delete expressions persist in the session.
repl_test!(delete_expression_persistence, |repl| {
    repl.sendln("uint256 value = 42");
    repl.sendln("delete value");
    repl.sendln("value");
    repl.expect("Decimal: 0");
});

// Issue #9377: Test correct hex formatting for uint256.
repl_test!(uint256_hex_formatting, |repl| {
    repl.sendln("uint256 x = 42");
    // Full word hex should be 64 chars (256 bits).
    repl.sendln("x");
    repl.expect("0x000000000000000000000000000000000000000000000000000000000000002a");
});

// Issue #9377: Test that full words are printed correctly.
repl_test!(full_word_hex_formatting, |repl| {
    repl.sendln(r#"uint256(keccak256(abi.encode(uint256(keccak256("AgoraStableSwapStorage.OracleStorage")) - 1))) & ~uint256(0xff)"#);
    repl.expect(
        "Hex (full word): 0x0a6b316b47a0cd26c1b582ae3dcffbd175283c221c3cb3d1c614e3e47f62a700",
    );
});

// Test that uint is printed properly with any size.
repl_test!(uint_formatting, |repl| {
    for size in (8..=256).step_by(8) {
        repl.sendln(&format!("type(uint{size}).max"));
        repl.expect(&format!("Hex: 0x{}", "f".repeat(size / 4)));

        repl.sendln(&format!("uint{size}(2)"));
        repl.expect("Hex: 0x2");
    }
});

// Test that int is printed properly with any size.
repl_test!(int_formatting, |repl| {
    for size in (8..=256).step_by(8) {
        let size_minus_1: usize = size / 4 - 1;
        repl.sendln(&format!("type(int{size}).max"));
        repl.expect(&format!("Hex: 0x7{}", "f".repeat(size_minus_1)));

        repl.sendln(&format!("int{size}(2)"));
        repl.expect("Hex: 0x2");

        repl.sendln(&format!("type(int{size}).min"));
        repl.expect(&format!("Hex: 0x8{}", "0".repeat(size_minus_1)));

        repl.sendln(&format!("int{size}(-2)"));
        repl.expect(&format!("Hex: 0x{}e", "f".repeat(size_minus_1)));
    }
});

repl_test!(uninitialized_variables, |repl| {
    repl.sendln("uint256 x;");
    repl.sendln("address y;");
    repl.sendln("assembly { y := not(x) }");

    repl.sendln("x");
    repl.expect("Hex: 0x0");

    repl.sendln("y");
    repl.expect("Data: 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF");
});

repl_test!(chisel_can_run_with_live_logs_flag, "--live-logs", init = true, |repl| {
    repl.sendln("import {console} from 'forge-std/Script.sol';");
    repl.sendln("console.log('Hello, World!');");
    repl.expect("Hello, World!");

    repl.sendln("console.log('Goodbye, World!');");
    repl.expect("Hello, World!"); // old log is also printed
    repl.expect("Goodbye, World!");
});
