// SPDX-License-Identifier: Apache-2.0

#![cfg(test)]

mod data_account;

use crate::sema::ast::{Expression, Parameter, Statement, TryCatch, Type};
use crate::sema::yul::ast::InlineAssembly;
use crate::{parse_and_resolve, sema::ast, FileResolver, Target};
use solang_parser::pt::Loc;
use std::ffi::{OsStr, OsString};
use std::path::PathBuf;

pub(crate) fn parse(src: &'static str) -> ast::Namespace {
    let mut cache = FileResolver::default();
    cache.set_file_contents("test.sol", src.to_string());

    let ns = parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::EVM);
    ns
}

#[test]
fn test_statement_reachable() {
    let loc = Loc::File(0, 1, 2);
    let test_cases: Vec<(Statement, bool)> = vec![
        (Statement::Underscore(loc), true),
        (
            Statement::Destructure(loc, vec![], Expression::BoolLiteral { loc, value: true }),
            true,
        ),
        (
            Statement::VariableDecl(
                loc,
                0,
                Parameter {
                    loc,
                    id: None,
                    ty: Type::Bool,
                    ty_loc: None,
                    indexed: false,
                    readonly: false,
                    infinite_size: false,
                    recursive: false,
                    annotation: None,
                },
                None,
            ),
            true,
        ),
        (
            Statement::Emit {
                loc,
                event_no: 0,
                event_loc: Loc::Builtin,
                args: vec![],
            },
            true,
        ),
        (
            Statement::Delete(
                loc,
                Type::Bool,
                Expression::BoolLiteral { loc, value: true },
            ),
            true,
        ),
        (Statement::Continue(loc), false),
        (Statement::Break(loc), false),
        (Statement::Return(loc, None), false),
        (
            Statement::If(
                loc,
                false,
                Expression::BoolLiteral { loc, value: false },
                vec![],
                vec![],
            ),
            false,
        ),
        (
            Statement::While(
                loc,
                true,
                Expression::BoolLiteral { loc, value: false },
                vec![],
            ),
            true,
        ),
        (
            Statement::DoWhile(
                loc,
                false,
                vec![],
                Expression::BoolLiteral { loc, value: true },
            ),
            false,
        ),
        (
            Statement::Expression(loc, true, Expression::BoolLiteral { loc, value: false }),
            true,
        ),
        (
            Statement::For {
                loc,
                reachable: false,
                init: vec![],
                cond: None,
                next: None,
                body: vec![],
            },
            false,
        ),
        (
            Statement::TryCatch(
                loc,
                true,
                TryCatch {
                    expr: Expression::BoolLiteral { loc, value: false },
                    returns: vec![],
                    ok_stmt: vec![],
                    errors: vec![],
                    catch_all: None,
                },
            ),
            true,
        ),
        (
            Statement::Assembly(
                InlineAssembly {
                    loc,
                    memory_safe: false,
                    body: vec![],
                    functions: std::ops::Range { start: 0, end: 0 },
                },
                false,
            ),
            false,
        ),
    ];

    for (test_case, expected) in test_cases {
        assert_eq!(test_case.reachable(), expected);
    }
}

#[test]
fn constant_overflow_checks() {
    let file = r#"
    contract test_contract {
        function test_params(uint8 usesa, int8 sesa) public returns(uint8) {
            return usesa + uint8(sesa);
        }

        function test_add(int8 input) public returns (uint8) {
            // value 133 does not fit into type int8.
            int8 add_ovf = 127 + 6;

            // negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.
            uint8 negative = 3 - 4;

            // value 133 does not fit into type int8.
            int8 mixed = 126 + 7 + input;

            // negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.
            return 1 - 2;
        }

        function test_mul(int8 input) public {
            // value 726 does not fit into type int8.
            int8 mul_ovf = 127 * 6;

            // value 882 does not fit into type int8.
            int8 mixed = 126 * 7 * input;
        }

        function test_shift(int8 input) public {
            // value 128 does not fit into type int8.
            // warning: left shift by 7 may overflow the final result.
            int8 mul_ovf = 1 << 7;

            // value 128 does not fit into type int8.
            // warning: left shift by 7 may overflow the final result
            int8 mixed = (1 << 7) + input;
        }

        function test_call() public {
            // negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.
            // value 129 does not fit into type int8.
            test_params(1 - 2, 127 + 2);

            // negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.
            // value 129 does not fit into type int8.
            test_params({usesa: 1 - 2, sesa: 127 + 2});
        }

        function test_builtin (bytes input) public{

            // value 4294967296 does not fit into type uint32.
            int16 sesa = input.readInt16LE(4294967296);
        }

        function test_for_loop () public {
            for (int8 i = 125 + 5; i < 300 ; i++) {
            }
        }

        function composite(int8 a, bytes input) public{

            uint8 sesa = 500- 400 + test_params(100+200, 0) + (200+101) + input.readUint8(4294967296);
            int8 seas = (120 + 120) + a + (120 + 125);

            // no diagnostic
            uint8 b = 255 - 255/5 ;

            // value 260 does not fit into type uint8.
            uint8 shift_r = (120 >> 2) + 230;

            // value 261 does not fit into type uint8.
            uint8 mod_test = 254 + (500%17);

            // value 269 does not fit into type uint8.
            uint8 bb = 320 - (255/5) ;

            // left shift by 7 may overflow the final result
            uint8 shift_warning = (1 << 9) - 300;

            int8 bitwise_or = (250 | 5) - 150;

            // value 155 does not fit into type int8.
            int8 bitwise_or_ovf = (250 | 5) - 100;

            uint8 bitwise_and = 1000 & 5 ;

            // value 262 does not fit into type uint8.
            uint8 bitwise_and_ovf = (1000 & 255) + 30 ;

            uint8 bitwise_xor = 1000 ^ 256;

            // divide by zero
            uint8 div_zero= 3 / (1-1);

            // divide by zero
            uint8 div_zeroo = (300-50) % 0;

            // shift by negative number not allowed.
            uint8 shift_left_neg = 120 << -1;
            uint8 shift_right_neg = 120 >> -1;

            // power by -1 is not allowed.
            uint8 pow = 12 ** -1;

            // large shift not allowed
            int x = 1 >> 14676683207225698178084221555689649093015162623576402558976;

        }
    }

        "#;
    let ns = parse(file);
    let errors = ns.diagnostics.errors();
    let warnings = ns.diagnostics.warnings();

    assert_eq!(errors[0].message, "value 133 does not fit into type int8.");
    assert_eq!(errors[1].message, "negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.");
    assert_eq!(errors[2].message, "value 133 does not fit into type int8.");
    assert_eq!(errors[3].message, "negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.");
    assert_eq!(errors[4].message, "value 762 does not fit into type int8.");
    assert_eq!(errors[5].message, "value 882 does not fit into type int8.");
    assert_eq!(errors[6].message, "value 128 does not fit into type int8.");
    assert_eq!(errors[7].message, "value 128 does not fit into type int8.");
    assert_eq!(errors[8].message, "negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.");
    assert_eq!(errors[9].message, "value 129 does not fit into type int8.");
    assert_eq!(errors[10].message, "negative value -1 does not fit into type uint8. Cannot implicitly convert signed literal to unsigned type.");
    assert_eq!(errors[11].message, "value 129 does not fit into type int8.");
    assert_eq!(
        errors[12].message,
        "value 4294967296 does not fit into type uint32."
    );
    assert_eq!(errors[13].message, "value 130 does not fit into type int8.");
    assert_eq!(
        errors[14].message,
        "value 300 does not fit into type uint8."
    );
    assert_eq!(
        errors[15].message,
        "value 301 does not fit into type uint8."
    );
    assert_eq!(
        errors[16].message,
        "value 4294967296 does not fit into type uint32."
    );
    assert_eq!(errors[17].message, "value 240 does not fit into type int8.");
    assert_eq!(errors[18].message, "value 245 does not fit into type int8.");
    assert_eq!(
        errors[19].message,
        "value 260 does not fit into type uint8."
    );
    assert_eq!(
        errors[20].message,
        "value 261 does not fit into type uint8."
    );
    assert_eq!(
        errors[21].message,
        "value 269 does not fit into type uint8."
    );
    assert_eq!(errors[22].message, "value 155 does not fit into type int8.");
    assert_eq!(
        errors[23].message,
        "value 262 does not fit into type uint8."
    );

    assert_eq!(
        errors[24].message,
        "value 744 does not fit into type uint8."
    );
    assert_eq!(errors[25].message, "divide by zero");
    assert_eq!(errors[26].message, "divide by zero");
    assert_eq!(errors[27].message, "left shift by -1 is not possible");
    assert_eq!(errors[28].message, "right shift by -1 is not possible");
    assert_eq!(errors[29].message, "power by -1 is not possible");
    assert_eq!(errors[30].message, "right shift by 14676683207225698178084221555689649093015162623576402558976 is not possible");

    assert_eq!(errors.len(), 31);

    assert_eq!(warnings.len(), 0);
}

#[test]
fn test_types() {
    let file = r#"
    contract test_contract {
        function test_types32(bytes input) public {
            // value 2147483648 does not fit into type int32.
            int32 add_ovf = 2147483647 + 1;

            // value 2147483648 does not fit into type int32.
            int32 add_normal = 2147483647 + 0;

            // value 2147483648 does not fit into type int32.
            int32 mixed = 2147483647 + 1 + input.readInt32LE(2);
        }

        function test_types64(bytes input) public {
            // value 9223372036854775808 does not fit into type int64.
            int64 add_ovf = 9223372036854775807 + 1;

            int64 add_normal = 9223372036854775807;

            // value 9223372036854775808 does not fit into type int64.
            int64 mixed = 9223372036854775807 + 1 + input.readInt64LE(2);

            // value 18446744073709551616 does not fit into type uint64.
            uint64 pow_ovf = 2**64;

            uint64 normal_pow = (2**64) - 1;
        }

        function test_types_128_256(bytes input) public {
            while (true) {
                // value 340282366920938463463374607431768211456 does not fit into type uint64.
                uint128 ovf = 2**128;
                uint128 normal = 2**128 - 1;
            }
            uint128[] arr;
            // negative value -1 does not fit into type uint32. Cannot implicitly convert signed literal to unsigned type.
            // value 340282366920938463463374607431768211456 does not fit into type uint128.
            uint128 access = arr[1 - 2] + 1 + (2**128);
            // value 3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 does not fit into type uint256.
            uint256 num = 3e255;

            uint256 num_2 = 115792089237316195423570985008687907853269984665640564039457584007913129639935 *
                    4;
        }

        function foo() public {
            uint16 x = 0;
            x += 450000;

            for (uint16 i = 0; i < (2**32); i += 65546) {}

            uint8 y = 0;
            y *= 120 + 250;
            y -= 500;
            y /= 300 + 200 - 200 + y;
        }
    }

        "#;
    let ns = parse(file);
    let errors = ns.diagnostics.errors();

    assert_eq!(
        errors[0].message,
        "value 2147483648 does not fit into type int32."
    );
    assert_eq!(
        errors[1].message,
        "value 2147483648 does not fit into type int32."
    );
    assert_eq!(
        errors[2].message,
        "value 9223372036854775808 does not fit into type int64."
    );
    assert_eq!(
        errors[3].message,
        "value 9223372036854775808 does not fit into type int64."
    );
    assert_eq!(
        errors[4].message,
        "value 18446744073709551616 does not fit into type uint64."
    );
    assert_eq!(
        errors[5].message,
        "value 340282366920938463463374607431768211456 does not fit into type uint128."
    );
    assert_eq!(errors[6].message, "negative value -1 does not fit into type uint32. Cannot implicitly convert signed literal to unsigned type.");
    assert_eq!(
        errors[7].message,
        "value 340282366920938463463374607431768211456 does not fit into type uint128."
    );

    assert_eq!(errors[8].message, "value 3000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 does not fit into type uint256.");

    assert_eq!(errors[9].message, "value 463168356949264781694283940034751631413079938662562256157830336031652518559740 does not fit into type uint256.");
    assert_eq!(
        errors[10].message,
        "value 450000 does not fit into type uint16."
    );
    assert_eq!(
        errors[11].message,
        "value 65546 does not fit into type uint16."
    );
    assert_eq!(
        errors[12].message,
        "value 370 does not fit into type uint8."
    );
    assert_eq!(
        errors[13].message,
        "value 500 does not fit into type uint8."
    );
    assert_eq!(
        errors[14].message,
        "value 300 does not fit into type uint8."
    );
    assert_eq!(errors.len(), 15);
}

#[test]
fn try_catch_solana() {
    let file = r#"
@program_id("GTW14QhXXafodyHp6RTaoVKKUgG4G2YgVYg2dBfp6FK4")
contract aborting {
    function abort() public returns (int32, bool) {
        revert("bar");
    }
}

contract runner {
    function test() external pure {

        try aborting.abort() returns (int32 a, bool b) {
            // call succeeded; return values are in a and b
        }
        catch Error(string x) {
            if (x == "bar") {
                // "bar" reason code was provided through revert() or require()
            }
        }
        catch (bytes raw) {
            // if no error string could decoding, we end up here with the raw data
        }
    }
}
    "#;

    let mut cache = FileResolver::default();
    cache.set_file_contents("test.sol", file.to_string());

    let ns = parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::Solana);

    assert_eq!(ns.diagnostics.len(), 3);
    assert!(ns.diagnostics.contains_message("found contract 'runner'"));
    assert!(ns.diagnostics.contains_message("found contract 'aborting'"));
    assert!(ns.diagnostics.contains_message("The try-catch statement is not \
     supported on Solana. Please, go to \
     https://solang.readthedocs.io/en/latest/language/statements.html#try-catch-statement for more information"));
}

#[test]
fn solana_discriminator_type() {
    let src = r#"
    contract test {
    function foo() public pure returns (int32) {
        return -3;
    }

    function testA() public returns (uint32) {
        function () external returns (int32) fptr = this.foo;
        return foo.selector;
    }

    function testB() public returns (bytes4) {
        function () external returns (int32) fptr = this.foo;
        return foo.selector;
    }
}
    "#;

    let mut cache = FileResolver::default();
    cache.set_file_contents("test.sol", src.to_string());

    let ns = parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::Solana);

    assert_eq!(ns.diagnostics.len(), 5);
    assert!(ns.diagnostics.contains_message("found contract 'test'"));
    assert!(ns.diagnostics.contains_message(
        "function selector needs an integer of at least 64 bits to avoid being truncated"
    ));
    assert!(ns
        .diagnostics
        .contains_message("implicit conversion to uint32 from bytes8 not allowed"));

    assert!(ns
        .diagnostics
        .contains_message("function selector should only be casted to bytes8 or larger"));
    assert!(ns
        .diagnostics
        .contains_message("implicit conversion would truncate from bytes8 to bytes4"));
}

#[test]
fn dynamic_account_metas() {
    let src = r#"
    import 'solana';

contract creator {
    function create_child_with_meta(address child, address payer) public {
        AccountMeta[] metas = new AccountMeta[](2);

        metas[0] = AccountMeta({pubkey: child, is_signer: false, is_writable: false});
        metas[1] = AccountMeta({pubkey: payer, is_signer: true, is_writable: true});

        Child.new{accounts: metas}(payer);

        Child.say_hello{accounts: []}();
    }
}

@program_id("Chi1d5XD6nTAp2EyaNGqMxZzUjh6NvhXRxbGHP3D1RaT")
contract Child {
    @payer(payer)
    @space(511 + 7)
    constructor(address payer) {
        print("In child constructor");
    }

    function say_hello() pure public {
        print("Hello there");
    }
}
    "#;
    let mut cache = FileResolver::default();
    cache.set_file_contents("test.sol", src.to_string());

    let ns = parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::Solana);

    let errors = ns.diagnostics.errors();
    assert_eq!(errors.len(), 1);
    assert_eq!(
        errors[0].message,
        "dynamic array is not supported for the 'accounts' argument"
    );
}

#[test]
fn no_address_and_no_metas() {
    let src = r#"
    import 'solana';

contract creator {
    function create_child_with_meta(address child) public {
        Child.new();
        Child.say_hello();
    }
}

@program_id("Chi1d5XD6nTAp2EyaNGqMxZzUjh6NvhXRxbGHP3D1RaT")
contract Child {
    @payer(payer)
    @space(511 + 7)
    constructor() {
        print("In child constructor");
    }

    function say_hello() pure public {
        print("Hello there");
    }
}
    "#;
    let mut cache = FileResolver::default();
    cache.set_file_contents("test.sol", src.to_string());

    let ns = parse_and_resolve(OsStr::new("test.sol"), &mut cache, Target::Solana);

    let errors = ns.diagnostics.errors();
    assert_eq!(errors.len(), 1);
    assert_eq!(
        errors[0].message,
        "accounts are required for calling a contract. You can either provide the accounts with the {accounts: ...} call argument or change this function's visibility to external"
    );
}

#[test]
fn get_import_map() {
    let mut cache = FileResolver::default();
    let map = OsString::from("@openzepellin");
    let example_sol_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("examples")
        .canonicalize()
        .unwrap();

    cache.add_import_map(map.clone(), example_sol_path.clone());

    let retrieved = cache.get_import_map(&map);
    assert_eq!(Some(&example_sol_path), retrieved);
}

#[test]
fn get_import_path() {
    let mut cache = FileResolver::default();
    let examples = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("examples")
        .canonicalize()
        .unwrap();

    let bad_path = PathBuf::from("/IDontExist.sol");

    cache.add_import_path(&examples);
    cache.add_import_path(&bad_path);

    let ns = parse_and_resolve(OsStr::new("example.sol"), &mut cache, Target::EVM);

    let file = ns.files.first();
    assert!(file.is_some());
    if let Some(file) = file {
        let import_path = cache.get_import_path(file.import_no.unwrap());
        assert_eq!(Some(&(None, examples.clone())), import_path);
    }

    let ns = parse_and_resolve(OsStr::new("incrementer.sol"), &mut cache, Target::EVM);
    let file = ns.files.first();
    assert!(file.is_some());
    if let Some(file) = file {
        let import_path = cache.get_import_path(file.import_no.unwrap());
        assert_eq!(Some(&(None, examples.clone())), import_path);
    }
}
