use javascript_globals::GLOBALS_BUILTIN;

use rustc_hash::FxHashSet;

use oxc_allocator::Allocator;
use oxc_ast::ast::{Expression, IdentifierReference, Statement};
use oxc_ecmascript::{
    GlobalContext,
    side_effects::{
        MayHaveSideEffects, MayHaveSideEffectsContext, PropertyReadSideEffects, is_pure_function,
    },
};
use oxc_parser::Parser;
use oxc_span::SourceType;

struct Ctx {
    global_variable_names: FxHashSet<&'static str>,
    annotation: bool,
    pure_function_names: Vec<String>,
    property_read_side_effects: PropertyReadSideEffects,
    property_write_side_effects: bool,
    unknown_global_side_effects: bool,
}

impl Default for Ctx {
    fn default() -> Self {
        Self {
            global_variable_names: GLOBALS_BUILTIN
                .keys()
                .copied()
                .chain(["arguments", "URL"])
                .collect::<FxHashSet<_>>(),
            annotation: true,
            pure_function_names: vec![],
            property_read_side_effects: PropertyReadSideEffects::All,
            property_write_side_effects: true,
            unknown_global_side_effects: true,
        }
    }
}

impl<'a> GlobalContext<'a> for Ctx {
    fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool {
        self.global_variable_names.contains(ident.name.as_str())
    }
}

impl MayHaveSideEffectsContext<'_> for Ctx {
    fn annotations(&self) -> bool {
        self.annotation
    }

    fn manual_pure_functions(&self, callee: &Expression) -> bool {
        is_pure_function(callee, &self.pure_function_names)
    }

    fn property_read_side_effects(&self) -> PropertyReadSideEffects {
        self.property_read_side_effects
    }

    fn property_write_side_effects(&self) -> bool {
        self.property_write_side_effects
    }

    fn unknown_global_side_effects(&self) -> bool {
        self.unknown_global_side_effects
    }
}

#[track_caller]
fn test(source_text: &str, expected: bool) {
    let ctx = Ctx::default();
    test_with_ctx(source_text, &ctx, expected);
}

#[track_caller]
fn test_ts(source_text: &str, expected: bool) {
    let ctx = Ctx::default();
    let allocator = Allocator::default();
    let ret = Parser::new(&allocator, source_text, SourceType::tsx()).parse();
    assert!(!ret.panicked, "{source_text}");
    assert!(ret.diagnostics.is_empty(), "{source_text}");

    let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
        panic!("should have a expression statement body: {source_text}");
    };
    assert_eq!(stmt.expression.may_have_side_effects(&ctx), expected, "{source_text}");
}

#[track_caller]
fn test_with_global_variables(
    source_text: &str,
    global_variable_names: &[&'static str],
    expected: bool,
) {
    let ctx = Ctx {
        global_variable_names: global_variable_names.iter().copied().collect(),
        ..Default::default()
    };
    test_with_ctx(source_text, &ctx, expected);
}

#[track_caller]
fn test_with_ctx(source_text: &str, ctx: &Ctx, expected: bool) {
    let allocator = Allocator::default();
    let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
    assert!(!ret.panicked, "{source_text}");
    assert!(ret.diagnostics.is_empty(), "{source_text}");

    let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
        panic!("should have a expression statement body: {source_text}");
    };
    assert_eq!(stmt.expression.may_have_side_effects(ctx), expected, "{source_text}");
}

#[track_caller]
fn test_in_function(source_text: &str, expected: bool) {
    let ctx = Ctx::default();
    let allocator = Allocator::default();
    let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
    assert!(!ret.panicked, "{source_text}");
    assert!(ret.diagnostics.is_empty(), "{source_text}");

    let Some(Statement::FunctionDeclaration(stmt)) = &ret.program.body.first() else {
        panic!("should have a function declaration: {source_text}");
    };
    let Some(Statement::ExpressionStatement(stmt)) =
        &stmt.body.as_ref().expect("should have a body").statements.first()
    else {
        panic!("should have a expression statement body: {source_text}");
    };

    assert_eq!(stmt.expression.may_have_side_effects(&ctx), expected, "{source_text}");
}

#[track_caller]
fn test_assign_target(source_text: &str, expected: bool) {
    test_assign_target_with_global_variables(source_text, &[], expected);
}

#[track_caller]
fn test_assign_target_with_global_variables(
    source_text: &str,
    global_variable_names: &[&'static str],
    expected: bool,
) {
    let ctx = Ctx {
        global_variable_names: global_variable_names.iter().copied().collect(),
        ..Default::default()
    };
    let allocator = Allocator::default();
    let ret = Parser::new(&allocator, source_text, SourceType::mjs()).parse();
    assert!(!ret.panicked, "{source_text}");
    assert!(ret.diagnostics.is_empty(), "{source_text}");

    let Some(Statement::ExpressionStatement(stmt)) = &ret.program.body.first() else {
        panic!("should have a expression statement body: {source_text}");
    };
    let Expression::AssignmentExpression(assign_expr) = &stmt.expression.without_parentheses()
    else {
        panic!("should have a assignment expression: {source_text}");
    };

    assert_eq!(assign_expr.left.may_have_side_effects(&ctx), expected, "{source_text}");
}

/// <https://github.com/google/closure-compiler/blob/v20240609/test/com/google/javascript/jscomp/AstAnalyzerTest.java#L362>
#[test]
fn closure_compiler_tests() {
    test("[1]", false);
    test("[1, 2]", false);
    test("i++", true);
    test("[b, [a, i++]]", true);
    test("i=3", true);
    test("[0, i=3]", true);
    test("b()", true);
    test("[1, b()]", true);
    test("b.b=4", true);
    test("b.b--", true);
    test("i--", true);
    test("a[0][i=4]", true);
    test("a += 3", true);
    test("a, b, z += 4", true);
    test("a ? c : d++", true);
    test("a ?? b++", true);
    test("a + c++", true);
    test("a + c - d()", true);
    test("a + c - d()", true);
    // test("function foo() {}", true);
    // test("class Foo {}", true);
    // test("while(true);", true);
    // test("if(true){a()}", true);
    // test("if(true){a}", false);
    test("(function() { })", false);
    test("(function() { i++ })", false);
    test("[function a(){}]", false);
    test("(class { })", false);
    test("(class { method() { i++ } })", false);
    test("(class { [computedName()]() {} })", true);
    test("(class { [computedName]() {} })", false);
    test("(class Foo extends Bar { })", false);
    test("(class extends foo() { })", true);
    test("a", false);
    test("a.b", true);
    test("a.b.c", true);
    test("[b, c, [d, [e]]]", false);
    test("({a: x, b: y, c: z})", false);
    test("({a, b, c})", false);
    test("/abc/gi", false);
    test("('a')", false); // wrapped with parentheses to avoid treated as a directive
    test("0", false);
    test("a + c", true);
    test("'c' + a[0]", true);
    test("a[0][1]", true);
    test("'a' + c", true);
    test("'a' + a.name", true);
    test("1, 2, 3", false);
    test("a, b, 3", false);
    test("(function(a, b) {  })", false);
    test("a ? c : d", false);
    test("a ?? b", false);
    // test("'1' + navigator.userAgent", false);
    test("`template`", false);
    test("`template${name}`", true);
    test("`${name}template`", true);
    test("`${naming()}template`", true);
    test("templateFunction`template`", true);
    test("st = `${name}template`", true);
    test("tempFunc = templateFunction`template`", true);
    // RegExp is validated using the regex parser to determine if it's pure.
    // Valid patterns are pure, invalid patterns have side effects (throw SyntaxError).
    // https://github.com/oxc-project/oxc/issues/18050
    test("new RegExp('foobar', 'i')", false); // Valid pattern and flags
    test("new RegExp('foobar', 2)", true); // Non-string flags, can't validate
    test("new RegExp(SomethingWacky(), 'i')", true); // Non-literal pattern, can't validate
    test("new RegExp('[')", true); // Invalid pattern
    test("new RegExp('a', 'xyz')", true); // Invalid flags
    // test("new Array()", false);
    // test("new Array", false);
    // test("new Array(4)", false);
    // test("new Array('a', 'b', 'c')", false);
    test("new SomeClassINeverHeardOf()", true);
    test("new SomeClassINeverHeardOf()", true);
    // test("({}).foo = 4", false);
    // test("([]).foo = 4", false);
    // test("(function() {}).foo = 4", false);
    test("this.foo = 4", true);
    test("a.foo = 4", true);
    test("(function() { return n; })().foo = 4", true);
    test("([]).foo = bar()", true);
    test("undefined", false);
    test("void 0", false);
    test("void foo()", true);
    test("-Infinity", false);
    test("Infinity", false);
    test("NaN", false);
    // test("({}||[]).foo = 2;", false);
    // test("(true ? {} : []).foo = 2;", false);
    // test("({},[]).foo = 2;", false);
    test("delete a.b", true);
    test("Math.random();", false);
    // `Math` is a known global, so accessing it is side-effect-free.
    test("Math.random(Math);", false);
    test_with_global_variables("Math.random(seed);", &["seed"], true);
    // test("[1, 1].foo;", false);
    // test("export var x = 0;", true);
    // test("export let x = 0;", true);
    // test("export const x = 0;", true);
    // test("export class X {};", true);
    // test("export function x() {};", true);
    // test("export {x};", true);

    // ARRAYLIT-ITER_SPREAD
    test("[...[]]", false);
    test("[...[1]]", false);
    test("[...[i++]]", true);
    test("[...'string']", false);
    test("[...`templatelit`]", false);
    test("[...`templatelit ${safe}`]", true);
    test("[...`templatelit ${unsafe()}`]", true);
    test("[...f()]", true);
    test("[...5]", true);
    test("[...null]", true);
    test("[...true]", true);

    // CALL-ITER_SPREAD
    // test("Math.sin(...[])", false);
    // test("Math.sin(...[1])", false);
    test("Math.sin(...[i++])", true);
    // test("Math.sin(...'string')", false);
    // test("Math.sin(...`templatelit`)", false);
    // test("Math.sin(...`templatelit ${safe}`)", true);
    test("Math.sin(...`templatelit ${unsafe()}`)", true);
    test("Math.sin(...f())", true);
    test("Math.sin(...5)", true);
    test("Math.sin(...null)", true);
    test("Math.sin(...true)", true);

    // NEW-ITER_SPREAD
    // test("new Object(...[])", false);
    // test("new Object(...[1])", false);
    test("new Object(...[i++])", true);
    // test("new Object(...'string')", false);
    // test("new Object(...`templatelit`)", false);
    // test("new Object(...`templatelit ${safe}`)", true);
    test("new Object(...`templatelit ${unsafe()}`)", true);
    test("new Object(...f())", true);
    test("new Object(...5)", true);
    test("new Object(...null)", true);
    test("new Object(...true)", true);

    // OBJECT_SPREAD
    // Spreading unknown identifiers could invoke getters.
    test("({...x})", true);
    // Spreading object literals is side-effect-free if the properties are.
    test("({...{}})", false);
    test("({...{a:1}})", false);
    test("({...{a:i++}})", true);
    test("({...{a:f()}})", true);
    test("({...f()})", true);

    // OBJECT_REST
    // This could invoke getters.
    test("({...x} = something)", true);
    // the presence of `a` affects what goes into `x`
    test("({a, ...x} = something)", true);

    // ITER_REST
    // We currently assume all iterable-rests are side-effectful.
    test("([...x] = 'safe')", true);
    test("(function(...x) { })", false);

    // Context switch
    // test("async function f() { await 0; }", true);
    // test("(async()=>{ for await (let x of []) {} })", true);
    // test("function f() { throw 'something'; }", true);
    // test("function* f() { yield 'something'; }", true);
    // test("function* f() { yield* 'something'; }", true);

    // Enhanced for loop
    // These edge cases are actually side-effect free. We include them to confirm we just give
    // up on enhanced for loops.
    // test("for (const x in []) { }", true);
    // test("for (const x of []) { }", true);

    // COMPUTED_PROP - OBJECTLIT
    test("({[a]: x})", false);
    test("({[a()]: x})", true);
    test("({[a]: x()})", true);

    // computed property getters and setters are modeled as COMPUTED_PROP with an
    // annotation to indicate getter or setter.
    test("({ get [a]() {} })", false);
    test("({ get [a()]() {} })", true);
    test("({ set [a](x) {} })", false);
    test("({ set [a()](x) {} })", true);

    // COMPUTED_PROP - CLASS
    test("(class C { [a]() {} })", false);
    test("(class C { [a()]() {} })", true);

    // computed property getters and setters are modeled as COMPUTED_PROP with an
    // annotation to indicate getter or setter.
    test("(class C { get [a]() {} })", false);
    test("(class C { get [a()]() {} })", true);
    test("(class C { set [a](x) {} })", false);
    test("(class C { set [a()](x) {} })", true);

    // GETTER_DEF
    test("({ get a() {} })", false);
    test("(class C { get a() {} })", false);

    // Getter use
    test("x.normal;", true);
    test("x?.normal;", true);
    test("({normal} = foo());", true);

    // SETTER_DEF
    test("({ set a(x) {} })", false);
    test("(class C { set a(x) {} })", false);

    // SETTER_USE
    test("x.normal = 0;", true);

    // MEMBER_FUNCTION_DEF
    test("({ a(x) {} })", false);
    test("(class C { a(x) {} })", false);

    // MEMBER_FIELD_DEF
    test("(class C { x=2; })", false);
    test("(class C { x; })", false);
    test("(class C { x })", false);
    test("(class C { x \n y })", false);
    test("(class C { static x=2; })", false);
    test("(class C { static x; })", false);
    test("(class C { static x })", false);
    test("(class C { static x \n static y })", false);
    test("(class C { x = alert(1); })", false);
    test("(class C { static x = alert(1); })", true);

    // COMPUTED_FIELD_DEF
    test("(class C { [x]; })", false);
    test("(class C { ['x']=2; })", false);
    test("(class C { 'x'=2; })", false);
    test("(class C { 1=2; })", false);
    test("(class C { static [x]; })", false);
    test("(class C { static ['x']=2; })", false);
    test("(class C { static 'x'=2; })", false);
    test("(class C { static 1=2; })", false);
    test("(class C { ['x'] = alert(1); })", false);
    test("(class C { static ['x'] = alert(1); })", true);
    test("(class C { static [alert(1)] = 2; })", true);

    // CLASS_STATIC_BLOCK
    test("(class C { static {} })", false);
    test("(class C { static { [1]; } })", false);
    test("(class C { static { let x; } })", false);
    test("(class C { static { const x =1 ; } })", false);
    test("(class C { static { var x; } })", false);
    test("(class C { static { this.x = 1; } })", true);
    test("(class C { static { function f() { } } })", false);
    test("(class C { static { (function () {} )} })", false);
    test("(class C { static { ()=>{} } })", false);

    // SUPER calls
    test("super()", true);
    test("super.foo()", true);

    // A RegExp Object by itself doesn't have any side-effects
    test("/abc/gi", false);

    // RegExp instance methods have global side-effects, so whether they are
    // considered side-effect free depends on whether the global properties
    // are referenced.
    test("(/abc/gi).test('')", true);
    test("(/abc/gi).test(a)", true);
    test("(/abc/gi).exec('')", true);

    // Some RegExp object method that may have side-effects.
    test("(/abc/gi).foo('')", true);

    // Try the string RegExp ops.
    test("''.match('a')", true);
    test("''.match(/(a)/)", true);
    test("''.replace('a')", true);
    test("''.search('a')", true);
    test("''.split('a')", true);

    // Some non-RegExp string op that may have side-effects.
    test("''.foo('a')", true);

    // 'a' might be a RegExp object with the 'g' flag, in which case
    // the state might change by running any of the string ops.
    // Specifically, using these methods resets the "lastIndex" if used
    // in combination with a RegExp instance "exec" method.
    test("''.match(a)", true);

    // Dynamic import changes global state
    test("import('./module.js')", true);
}

#[test]
fn test_identifier_reference() {
    // accessing global variables may have a side effect
    test_with_global_variables("a", &["a"], true);
    // accessing known globals are side-effect free
    test("NaN", false);
}

#[test]
fn test_simple_expressions() {
    test("1n", false);
    test("true", false);
    test("import.meta", false);
    test("(() => {})", false);

    // referencing `this` in a derived class before `super()` is called causes a ReferenceError
    test("this", true);
}

#[test]
fn test_template_literal() {
    test("``", false);
    test("`a`", false);
    test("`${1}`", false);
    test("`${[]}`", false);
    test("`${Symbol()}`", true);
    test("`${{ toString() { console.log('sideeffect') } }}`", true);
    test("`${{ valueOf() { console.log('sideeffect') } }}`", true);
    test("`${{ [s]() { console.log('sideeffect') } }}`", true); // assuming s is Symbol.toPrimitive
    test("`${a}`", true); // a maybe a symbol
    test("`${a()}`", true);
    test("`${a() === b}`", true);
}

#[test]
fn test_unary_expressions() {
    test("delete 'foo'", true);
    test("delete foo()", true);

    test("void 'foo'", false);
    test("void foo()", true);
    test("!'foo'", false);
    test("!foo()", true);

    test("typeof 'foo'", false);
    test_with_global_variables("typeof a", &["a"], false);
    test_with_global_variables("typeof (0, a)", &["a"], true);
    test("typeof foo()", true);

    test("+0", false);
    test("+0n", true);
    test("+null", false); // 0
    test("+true", false); // 1
    test("+'foo'", false); // NaN
    test("+`foo`", false); // NaN
    test("+/foo/", false); // NaN
    test("+Infinity", false);
    test("+NaN", false);
    test("+undefined", false); // NaN
    test("+[]", false); // 0
    test("+[foo()]", true);
    test("+foo()", true);
    test("+foo", true); // foo can be Symbol or BigInt
    test("+Symbol()", true);
    test("+{}", false); // NaN
    test("+{ valueOf() { return Symbol() } }", true);

    test("-0", false);
    test("-0n", false);
    test("-null", false); // -0
    test("-true", false); // -1
    test("-'foo'", false); // -NaN
    test("-`foo`", false); // NaN
    test("-/foo/", false); // NaN
    test("-Infinity", false);
    test("-NaN", false);
    test("-undefined", false); // NaN
    test("-[]", false); // -0
    test("-[foo()]", true);
    test("-foo()", true);
    test("-foo", true); // foo can be Symbol
    test("-Symbol()", true);
    test("-{}", false); // NaN
    test("-{ valueOf() { return Symbol() } }", true);

    test("~0", false);
    test("~'foo'", false);
    test("~foo()", true);
    test("~foo", true);
}

#[test]
fn test_logical_expressions() {
    test("a || b", false);
    test("a() || b", true);
    test("a && b", false);
    test("a() && b", true);
    test("a ?? b", false);
    test("a() ?? b", true);
}

#[test]
fn test_other_expressions() {
    test("(foo)", false);
    test("(foo())", true);

    test("a ? b : c", false);
    test("a() ? b : c", true);

    test("a, b", false);
    test("a(), b", true);
    test("a, b()", true);
}

#[test]
fn test_binary_expressions() {
    test("a === b", false);
    test("a() === b", true);
    test("a !== b", false);
    test("a() !== b", true);

    test("a == b", false);
    test("a() == b", true);
    // These actually have a side effect, but this treated as side-effect free.
    test("'' == { toString() { console.log('sideeffect') } }", false);
    test("'' == { valueOf() { console.log('sideeffect') } }", false);
    test("'' == { [s]() { console.log('sideeffect') } }", false); // assuming s is Symbol.toPrimitive
    test("a != b", false);
    test("a() != b", true);

    test("a < b", false);
    test("a() < b", true);
    // These actually have a side effect, but this treated as side-effect free.
    test("'' < { toString() { console.log('sideeffect') } }", false);
    test("'' < { valueOf() { console.log('sideeffect') } }", false);
    test("'' < { [s]() { console.log('sideeffect') } }", false); // assuming s is Symbol.toPrimitive
    test("a > b", false);
    test("a() > b", true);
    test("a >= b", false);
    test("a() >= b", true);
    test("a <= b", false);
    test("a() <= b", true);

    test("'' + ''", false);
    test("'' + ``", false);
    test("'' + `${foo()}`", true);
    test("'' + null", false);
    test("'' + 0", false);
    test("'' + 0n", false);
    test("'' + true", false);
    test("'' + /a/", false);
    test("'' + []", false);
    test("'' + [foo()]", true);
    test("'' + Symbol()", true);
    test("'' + Infinity", false);
    test("'' + NaN", false);
    test("'' + undefined", false);
    test("'' + s", true); // assuming s is Symbol
    test("Symbol() + ''", true);
    test("'' + {}", false);
    test("'' + { toString() { return Symbol() } }", true);
    test("'' + { valueOf() { return Symbol() } }", true);
    test("'' + { [s]() { return Symbol() } }", true); // assuming s is Symbol.toPrimitive
    test("/a/ + 1", false); // /a/1
    test("[] + 1", false); // 1
    test("({} + 1)", false); // [object Object]1
    test("0 + 1", false);
    test("0 + null", false); // 0
    test("0 + true", false); // 1
    test("0 + a", true); // a can be BigInt
    test("0n + 1n", false);
    test("0n + a", true); // a can be Number
    test("a + b", true);

    test("0n - 1n", false);
    test("0n - 0", true);
    test("0n - a", true); // a can be Number
    test("a - 0n", true); // a can be Number
    test("0n - a()", true);
    test("0 - 1", false);
    test("0 - a", true); // a can be BigInt
    test("0 - ''", false); // 0
    test("0 - ``", false); // 0
    test("0 - true", false); // -1
    test("0 - /a/", false); // NaN
    test("0 - []", false); // 0
    test("0 - [foo()]", true);
    test("0 - Infinity", false); // -Infinity
    test("0 - NaN", false); // NaN
    test("0 - undefined", false); // NaN
    test("null - Infinity", false); // -Infinity
    test("0 - {}", false); // NaN
    test("'' - { toString() { return Symbol() } }", true);
    test("'' - { valueOf() { return Symbol() } }", true);
    test("'' - { [s]() { return Symbol() } }", true); // assuming s is Symbol.toPrimitive
    test("a - b", true);
    test("0 * 1", false);
    test("0 * a", true);
    test("0 / 1", false);
    test("0 / a", true);
    test("0 % 1", false);
    test("0 % a", true);
    test("0 << 1", false);
    test("0 << a", true);
    test("0 | 1", false);
    test("0 | a", true);
    test("0 >> 1", false);
    test("0 >> a", true);
    test("0 ^ 1", false);
    test("0 ^ a", true);
    test("0 & 1", false);
    test("0 & a", true);
    test("0 ** 1", false);
    test("0 ** a", true);
    test("1n ** (-1n)", true); // `**` throws an error when the right operand is negative
    test("1n / 0n", true); // `/` throws an error when the right operand is zero
    test("1n % 0n", true); // `%` throws an error when the right operand is zero
    test("0n >>> 1n", true); // `>>>` throws an error even when both operands are bigint

    test("[] instanceof 1", true); // throws an error
    test("[] instanceof { [Symbol.hasInstance]() { throw 'foo' } }", true);
    test("[] instanceof Object", false);
    test("a instanceof Object", true); // a maybe a proxy that has a side effectful "getPrototypeOf" trap

    // b maybe not a object
    // b maybe a proxy that has a side effectful "has" trap
    test("a in b", true);
}

#[test]
fn test_object_expression() {
    // wrapped with parentheses to avoid treated as a block statement
    test("({})", false);
    test("({a: 1})", false);
    test("({a: foo()})", true);
    test("({1: 1})", false);
    test("({[1]: 1})", false);
    test("({[1n]: 1})", false);
    test("({['1']: 1})", false);
    // These actually have a side effect, but this treated as side-effect free.
    test("({[{ toString() { console.log('sideeffect') } }]: 1})", false);
    test("({[{ valueOf() { console.log('sideeffect') } }]: 1})", false);
    test("({[{ [s]() { console.log('sideeffect') } }]: 1})", false); // assuming s is Symbol.toPrimitive
    test("({[foo]: 1})", false);
    test("({[foo()]: 1 })", true);
    test("({a: foo()})", true);
    test("({...a})", true);
    test("({...[]})", false);
    test("({...[...a]})", true);
    test("({...'foo'})", false);
    test("({...`foo`})", false);
    test("({...`foo${1}`})", false);
    test("({...`foo${foo}`})", true);
    test("({...`foo${foo()}`})", true);
    test("({...foo()})", true);
    // Spreading object literals
    test("({...{}})", false);
    test("({...{a: 1}})", false);
    test("({...{a: foo()}})", true);
    test("({...{[foo()]: 1}})", true);
    // Getters are invoked when spreading
    test("({...{get a() {}}})", true);
    test("({...{get a() { return 1 }}})", true);
    // Setters are NOT invoked when spreading
    test("({...{set a(v) {}}})", false);
}

#[test]
fn test_array_expression() {
    test("[]", false);
    test("[1]", false);
    test("[foo()]", true);
    test("[,]", false);
    test("[...a]", true);
    test("[...[]]", false);
    test("[...[...a]]", true);
    test("[...'foo']", false);
    test("[...`foo`]", false);
    test("[...`foo${1}`]", false);
    test("[...`foo${foo}`]", true);
    test("[...`foo${foo()}`]", true);
    test("[...foo()]", true);
    // test_in_function("[...arguments]", true);
    test_in_function("function foo() { [...arguments] }", false);
}

#[test]
fn test_class_expression() {
    test("(class {})", false);
    test("(@foo class {})", true);
    test("(class extends a {})", false); // this may have a side effect, but ignored by the assumption
    test("(class extends foo() {})", true);
    test("(class extends (() => {}) {})", true);
    test("(class { static {} })", false);
    test("(class { static { 1; } })", false);
    test("(class { static { foo(); } })", true);
    test("(class { a() {} })", false);
    test("(class { [1]() {} })", false);
    test("(class { [1n]() {} })", false);
    test("(class { #a() {} })", false);
    test("(class { [foo()]() {} })", true);
    test("(class { @foo a() {} })", true);
    test("(class { a; })", false);
    test("(class { 1; })", false);
    test("(class { [1]; })", false);
    test("(class { [1n]; })", false);
    test("(class { #a; })", false);
    test("(class { @foo a; })", true);
    test("(class { [foo()] = 1 })", true);
    test("(class { a = foo() })", false); // foo() is called when constructed
    test("(class { static a; })", false);
    test("(class { static 1; })", false);
    test("(class { static [1]; })", false);
    test("(class { static [1n]; })", false);
    test("(class { static #a; })", false);
    test("(class { static [foo()] = 1 })", true);
    test("(class { static a = foo() })", true);
    test("(class { accessor [foo()]; })", true);
    test("(class { static accessor [foo()]; })", true);
    // AccessorProperty with value
    test("(class { accessor a = 1; })", false);
    test("(class { accessor a = foo(); })", true);
    test("(class { static accessor a = 1; })", false);
    test("(class { static accessor a = foo(); })", true);
    test("(class { #x; static { #x in {}; } })", false);
    test("(class { #x; static { #x in foo(); } })", true);
    // MethodDefinition with parameter decorators (TypeScript feature)
    test_ts("(class { a(@foo x) {} })", true);
    test_ts("(class { a(@foo x, @bar y) {} })", true);
    test_ts("(class { a(x) {} })", false);
}

#[test]
fn test_property_access() {
    test("a.length", true);
    test("a?.length", true);
    test("'a'.length", false);
    test("'a'?.length", false);
    test("[].length", false);
    test("[]['length']", false);
    test("[][`length`]", false);
    test("(foo() + '').length", true);

    test("a[0]", true);
    test("''[-1]", true); // String.prototype[-1] may be overridden
    test("''[0.3]", true); // String.prototype[0.3] may be overridden
    test("''[0]", true); // String.prototype[0] may be overridden
    test("'a'[0]", false);
    test("'a'[0n]", false);
    test("'a'[1]", true); // String.prototype[1] may be overridden
    test("'あ'[0]", false);
    test("'あ'[1]", true); // the length of "あ" is 1
    test("'😀'[0]", false);
    test("'😀'[1]", false);
    test("'😀'[2]", true); // the length of "😀" is 2

    test("[][-1]", true); // Array.prototype[-1] may be overridden
    test("[][0.3]", true); // Array.prototype[0.3] may be overridden
    test("[][0]", true); // Array.prototype[0] may be overridden
    test("[1][0]", false);
    test("[1][0n]", false);
    test("[1][1]", true); // Array.prototype[1] may be overridden
    test("[,][0]", false);
    test("[...[], 1][0]", false);
    test("[...[1]][0]", false);
    test("[...'a'][0]", false);
    test("[...'a'][1]", true);

    test("import.meta.url", true);
    test("import.meta['url']", true);
    test("import.meta[`url`]", true);
    test_in_function("function f() { new.target.url }", true);
    test("[...'😀'][0]", false);
    test("[...'😀'][1]", true);
    test("[...a, 1][0]", true); // "...a" may have a sideeffect
}

/// Tests for known global identifiers and property reads.
/// Ported from Rolldown's global_reference.rs / GLOBAL_IDENT set.
#[test]
fn test_known_global_identifiers() {
    // Known globals (in GLOBALS_BUILTIN) should be side-effect-free to access
    test("Math", false);
    test("Array", false);
    test("Object", false);
    test("JSON", false);
    test("Reflect", false);
    test("Symbol", false);
    test("Promise", false);
    test("Map", false);
    test("Set", false);
    test("WeakMap", false);
    test("WeakSet", false);
    test("parseInt", false);
    test("parseFloat", false);
    test("isNaN", false);
    test("isFinite", false);
    test("encodeURI", false);
    test("decodeURI", false);
    test("globalThis", false);

    // Browser/node globals need to be explicitly included in the global set
    test_with_global_variables("console", &["console"], false);
    test_with_global_variables("document", &["document"], false);
    test_with_global_variables("window", &["window"], false);
    test_with_global_variables("fetch", &["fetch"], false);

    // Unknown globals should have side effects when marked as global references
    test_with_global_variables("SomeUnknownGlobal", &["SomeUnknownGlobal"], true);
}

#[test]
fn test_known_global_property_reads() {
    // Math properties
    test("Math.PI", false);
    test("Math.E", false);
    test("Math.abs", false);
    test("Math.floor", false);
    test("Math.random", false);
    test("Math.unknownProp", true);

    // Object properties
    test("Object.keys", false);
    test("Object.create", false);
    test("Object.assign", false);
    test("Object.prototype", false);
    test("Object.unknownProp", true);

    // Reflect properties
    test("Reflect.apply", false);
    test("Reflect.get", false);
    test("Reflect.unknownProp", true);

    // Symbol properties
    test("Symbol.iterator", false);
    test("Symbol.asyncIterator", false);
    test("Symbol.unknownProp", true);

    // JSON properties
    test("JSON.parse", false);
    test("JSON.stringify", false);
    test("JSON.unknownProp", true);

    // console needs to be explicitly in the global set
    test_with_global_variables("console.log", &["console"], false);
    test_with_global_variables("console.error", &["console"], false);
    test_with_global_variables("console.warn", &["console"], false);
    test_with_global_variables("console.unknownMethod", &["console"], true);
}

#[test]
fn test_known_global_property_deep() {
    // 3-level chains on Object.prototype
    test("Object.prototype.hasOwnProperty", false);
    test("Object.prototype.isPrototypeOf", false);
    test("Object.prototype.toString", false);
    test("Object.prototype.valueOf", false);
    test("Object.prototype.propertyIsEnumerable", false);
    test("Object.prototype.unknownProp", true);

    // Non-Object 3-level chains are not supported
    test("Math.PI.toString", true);
    test("Array.prototype.push", true);
}

// `[ValueProperties]: PURE` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
#[test]
fn test_new_expressions() {
    test("new AggregateError", true);
    test("new DataView", true);
    test("new Symbol", true);
    test("new Set", false);
    test("new Map", false);
    test("new WeakSet", false);
    test("new WeakMap", false);
    test("new ArrayBuffer", false);
    test("new Date", false);
    test("new Boolean", false);
    test("new Error", false);
    test("new EvalError", false);
    test("new RangeError", false);
    test("new ReferenceError", false);
    // RegExp() with no arguments is valid (returns /(?:)/)
    test("new RegExp", false);
    test("new SyntaxError", false);
    test("new TypeError", false);
    test("new URIError", false);
    test("new Number", false);
    test("new Object", false);
    test("new String", false);

    // TypedArray constructors (0 args)
    test("new Int8Array", false);
    test("new Uint8Array", false);
    test("new Uint8ClampedArray", false);
    test("new Int16Array", false);
    test("new Uint16Array", false);
    test("new Int32Array", false);
    test("new Uint32Array", false);
    test("new Float32Array", false);
    test("new Float64Array", false);
    test("new BigInt64Array", false);
    test("new BigUint64Array", false);
    // DataView requires an ArrayBuffer argument; calling without one throws
    test("new DataView", true);

    // --- String: ToPrimitive assumed pure, but ToString(Symbol) throws ---
    // (Note: `String()` as function has special Symbol handling, but `new String()`
    // as constructor calls ToString which throws on Symbol.)
    test("new String()", false);
    test("new String('hello')", false);
    test("new String(123)", false);
    test("new String(true)", false);
    test("new String(null)", false);
    test("new String(x)", true); // unknown -> could be Symbol
    // Plain {} ToPrimitive returns "[object Object]" (string, not Symbol) -> safe
    test("new String({})", false);
    test("new String({toString() { return 'x' }})", true); // custom toString -> ToPrimitive undetermined

    // --- Number: ToPrimitive assumed pure, but ToNumber(Symbol) throws ---
    test("new Number()", false);
    test("new Number(123)", false);
    test("new Number('42')", false);
    test("new Number(true)", false);
    test("new Number(null)", false);
    test("new Number(x)", true); // unknown -> could be Symbol
    // Plain {} ToPrimitive returns "[object Object]" (string, not Symbol) -> safe
    test("new Number({})", false);
    test("new Number({valueOf() { return 1 }})", true); // custom valueOf -> ToPrimitive undetermined

    // --- Date: ToPrimitive assumed pure, but ToNumber(Symbol) and ToNumber(BigInt) throw ---
    test("new Date()", false);
    test("new Date(0)", false);
    test("new Date('2024')", false);
    test("new Date(x)", true); // unknown -> could be Symbol or BigInt
    test("new Date(0n)", true); // ToNumber(BigInt) throws

    // --- ArrayBuffer: ToIndex -> ToNumber, Symbol and BigInt throw ---
    test("new ArrayBuffer()", false);
    test("new ArrayBuffer(16)", false);
    test("new ArrayBuffer(x)", true); // unknown -> could be Symbol or BigInt
    test("new ArrayBuffer(0n)", true); // ToNumber(BigInt) throws

    // --- TypedArray with args ---
    // TypedArray: 0 args safe; with object arg calls @@iterator, BigInt arg throws in ToNumber
    test("new Uint8Array(16)", false); // numeric arg = length, safe
    test("new Int8Array(x)", true); // unknown value type
    test("new Float64Array({})", true); // object -> @@iterator may throw
    test("new Float64Array({[Symbol.iterator]() {}})", true); // custom iterator -> side effect

    // --- Object: always safe (wraps/returns any arg) ---
    test("new Object(x)", false);
    test("new Object({})", false);

    // --- Boolean: ToBoolean is internal, no user code ---
    test("new Boolean(x)", false);
    test("new Boolean({})", false);

    // --- Error types: ToString(Symbol) throws ---
    test("new Error()", false);
    test("new Error('msg')", false);
    test("new Error(x)", true); // unknown -> could be Symbol
    test("new TypeError(x)", true); // unknown -> could be Symbol
    test("new Error({})", false); // ToPrimitive returns string, safe

    // Collection constructors iterate their argument via Symbol.iterator,
    // so they are only pure with no args, null, undefined, or array literals.
    // null/undefined — pure
    test("new Set(null)", false);
    test("new Map(null)", false);
    test("new WeakSet(null)", false);
    test("new WeakMap(null)", false);
    test("new Set(undefined)", false);
    test("new Map(undefined)", false);
    // Array literal — pure
    test("new Set([])", false);
    test("new Set([1, 2, 3])", false);
    test("new Map([])", false);
    test("new Map([[1, 2], [3, 4]])", false);
    test("new WeakSet([])", false);
    test("new WeakMap([])", false);
    test("new WeakMap([[{}, 1]])", false);
    // Variable reference — NOT pure (could have custom Symbol.iterator)
    test("new Set(x)", true);
    test("new Map(x)", true);
    test("new WeakSet(x)", true);
    test("new WeakMap(x)", true);
    // Non-array literals — NOT pure
    test("new Set(false)", true);
    test("new Map({})", true);
    // Map with non-array entries — NOT pure
    test("new Map([x])", true);
    test("new Map([x, []])", true);
    test("new Map([[], x])", true);
}

// `PF` in <https://github.com/rollup/rollup/blob/master/src/ast/nodes/shared/knownGlobals.ts>
#[test]
fn test_call_expressions() {
    test("AggregateError()", true);
    test("DataView()", true);
    test("Set()", true);
    test("Map()", true);
    test("WeakSet()", true);
    test("WeakMap()", true);
    test("ArrayBuffer()", true);
    test("Date()", false);
    test("Boolean()", false);
    // Error constructors: ToString(Symbol) throws, but no args is safe
    test("Error()", false);
    test("Error('msg')", false);
    test("Error(x)", true); // unknown -> could be Symbol
    test("EvalError()", false);
    test("RangeError()", false);
    test("ReferenceError()", false);
    // RegExp() with no arguments is valid (returns /(?:)/)
    test("RegExp()", false);
    test("SyntaxError()", false);
    test("TypeError()", false);
    test("URIError()", false);
    test("Number()", false);
    test("Object()", false);
    test("String()", false);
    test("Symbol()", false);
    // String() — unconditionally pure (special Symbol handling + coercion assumption)
    test("String({})", false);
    test("String([1, 2, 3])", false);
    test("String({ toString() { return 'x' } })", false); // coercion assumed pure
    test("String(obj)", false); // coercion assumed pure
    // Number() — ToPrimitive assumed pure, but ToNumeric(Symbol) throws
    test("Number({})", false);
    test("Number({ valueOf() { return 1 } })", true); // custom valueOf -> ToPrimitive undetermined
    test("Number(Symbol())", true); // ToNumeric(Symbol) throws
    test("Number(obj)", true); // unknown -> could be Symbol
    test("Boolean({})", false);
    test("Boolean(obj)", false);
    // BigInt() — throws for invalid values
    test("BigInt()", true);
    test("BigInt(123)", false);
    test("BigInt(123n)", false);
    test("BigInt(true)", false);
    test("BigInt(false)", false);
    test("BigInt('456')", false);
    test("BigInt('abc')", true);
    test("BigInt(1.5)", true);
    test("BigInt(undefined)", true);
    test("BigInt(null)", true);
    test("BigInt({})", true);
    test("BigInt({ valueOf() { return 1 } })", true);
    test("BigInt(obj)", true);
    // Symbol() — ToPrimitive assumed pure, but ToString(Symbol) throws
    test("Symbol({})", false);
    test("Symbol({ toString() { return 'x' } })", true); // custom toString -> ToPrimitive undetermined
    test("Symbol(obj)", true); // unknown -> could be Symbol
    test("Symbol(Symbol())", true); // ToString(Symbol) throws

    test("decodeURI()", false);
    test("decodeURIComponent()", false);
    test("encodeURI()", false);
    test("encodeURIComponent()", false);
    test("escape()", false);
    test("isFinite()", false);
    test("isNaN()", false);
    test("parseFloat()", false);
    test("parseInt()", false);

    test("Array.isArray()", false);
    test("Array.of()", false);

    test("ArrayBuffer.isView()", false);

    test("Date.now()", false);
    test("Date.parse()", false);
    test("Date.UTC()", false);

    test("Math.abs()", false);
    test("Math.acos()", false);
    test("Math.acosh()", false);
    test("Math.asin()", false);
    test("Math.asinh()", false);
    test("Math.atan()", false);
    test("Math.atan2()", false);
    test("Math.atanh()", false);
    test("Math.cbrt()", false);
    test("Math.ceil()", false);
    test("Math.clz32()", false);
    test("Math.cos()", false);
    test("Math.cosh()", false);
    test("Math.exp()", false);
    test("Math.expm1()", false);
    test("Math.floor()", false);
    test("Math.fround()", false);
    test("Math.hypot()", false);
    test("Math.imul()", false);
    test("Math.log()", false);
    test("Math.log10()", false);
    test("Math.log1p()", false);
    test("Math.log2()", false);
    test("Math.max()", false);
    test("Math.min()", false);
    test("Math.pow()", false);
    test("Math.random()", false);
    test("Math.round()", false);
    test("Math.sign()", false);
    test("Math.sin()", false);
    test("Math.sinh()", false);
    test("Math.sqrt()", false);
    test("Math.tan()", false);
    test("Math.tanh()", false);
    test("Math.trunc()", false);

    test("Number.isFinite()", false);
    test("Number.isInteger()", false);
    test("Number.isNaN()", false);
    test("Number.isSafeInteger()", false);
    test("Number.parseFloat()", false);
    test("Number.parseInt()", false);

    // These throw on the `undefined` receiver (`ToObject(undefined)`) or are
    // conservatively kept; `Object.is()` is `Object.is(undefined, undefined)` -> true.
    test("Object.create()", true);
    test("Object.getOwnPropertyDescriptor()", true);
    test("Object.getOwnPropertyDescriptors()", true);
    test("Object.getOwnPropertyNames()", true);
    test("Object.getOwnPropertySymbols()", true);
    test("Object.getPrototypeOf()", true);
    test("Object.hasOwn()", true);
    test("Object.is()", false);
    test("Object.keys()", true);
    // `isExtensible`/`isFrozen`/`isSealed` return a primitive for a non-object receiver
    // (`Object.isExtensible(undefined)` -> `false`) instead of throwing, so they are pure.
    test("Object.isExtensible()", false);
    test("Object.isFrozen()", false);
    test("Object.isSealed()", false);

    test("String.fromCharCode()", false);
    test("String.fromCodePoint()", false);
    // `String.raw(undefined)` reads `undefined.raw` -> throws TypeError.
    test("String.raw()", true);

    test("Symbol.for()", false);
    // `Symbol.keyFor(undefined)` requires a Symbol argument -> throws TypeError.
    test("Symbol.keyFor()", true);

    // `URL.canParse()` has a required first argument -> throws TypeError with none.
    test("URL.canParse()", true);

    test("BigInt64Array.of()", false);
    test("BigUint64Array.of()", false);
    test("Float32Array.of()", false);
    test("Float64Array.of()", false);
    test("Int16Array.of()", false);
    test("Int32Array.of()", false);
    test("Int8Array.of()", false);
    test("Uint16Array.of()", false);
    test("Uint32Array.of()", false);
    test("Uint8Array.of()", false);
    test("Uint8ClampedArray.of()", false);

    // may have side effects if shadowed
    test_with_global_variables("Date()", &[], true);
    test_with_global_variables("Object.create()", &[], true);
}

#[test]
fn test_proxy_sensitive_object_methods() {
    // Pure with a determined, non-Proxy, non-null target (a literal cannot be a Proxy
    // and these methods never `[[Get]]`, so a literal getter is not invoked).
    test("Object.keys({})", false);
    test("Object.keys([])", false);
    test("Object.keys(42)", false);
    test("Object.keys('s')", false);
    test("Object.keys(true)", false);
    test("Object.keys(10n)", false);
    test("Object.keys({ get a() { f() } })", false);
    test("Object[\"keys\"]({})", false);
    test("Object.getOwnPropertyDescriptor({}, 'x')", false);
    test("Object.getOwnPropertyDescriptors({})", false);
    test("Object.getOwnPropertyNames({})", false);
    test("Object.getOwnPropertySymbols({})", false);
    test("Object.getPrototypeOf({})", false);
    test("Object.hasOwn({}, 'x')", false);
    test("Object.isExtensible({})", false);
    test("Object.isFrozen({})", false);
    test("Object.isSealed({})", false);

    // `isExtensible`/`isFrozen`/`isSealed` never `ToObject` their target: a non-object
    // (missing/null/undefined/primitive) yields a primitive result without throwing, so
    // they stay pure where `keys`/`getOwnPropertyDescriptor`/... must be kept.
    test("Object.isExtensible(null)", false);
    test("Object.isFrozen(undefined)", false);
    test("Object.isSealed(42)", false);
    // A Proxy target still fires the `[[IsExtensible]]`/`[[OwnPropertyKeys]]` traps, so an
    // undetermined target (could be a Proxy) or an inline Proxy is kept.
    test("Object.isExtensible(x)", true);
    test("Object.isFrozen(new Proxy({}, {}))", true);
    test("Object.isSealed(...x)", true);

    // Kept when the target could be a Proxy (undetermined) or would throw (null/undefined).
    test("Object.keys(x)", true);
    test("Object.keys(new Proxy({}, {}))", true);
    test("Object.keys(null)", true);
    test("Object.keys(undefined)", true);
    test("Object.keys(...x)", true);
    test("Object.getOwnPropertyDescriptor(x, 'x')", true);
    test("Object.getPrototypeOf(x)", true);
    test("Object.hasOwn(x, 'x')", true);
    test("Object[\"keys\"](x)", true);

    // `Object.values`/`entries` invoke `[[Get]]` (run getters), so they are always kept.
    test("Object.values({})", true);
    test("Object.entries({})", true);
    test("Object.values(x)", true);
    test("Object.values({ get a() { f() } })", true);

    // `Object.is` is unconditionally pure (never introspects its arguments).
    test("Object.is(1, 2)", false);
    test("Object.is(f(), 2)", true);

    // `Object.create(proto)` is pure with an object/null prototype and no properties.
    test("Object.create({})", false);
    test("Object.create([])", false);
    test("Object.create(null)", false);
    test("Object.create(x)", true); // proto could be a non-object -> throws
    test("Object.create(42)", true); // primitive proto -> throws
    test("Object.create(undefined)", true); // throws
    test("Object.create({}, x)", true); // properties read via [[OwnPropertyKeys]]/[[Get]]
    test("Object.create({}, {})", true);
}

/// Known-pure global functions/methods that nonetheless *throw* for some arguments,
/// so the call is not side-effect-free and must not be dropped. Cross-checked against
/// terser and esbuild, which both keep these.
#[test]
fn test_throwing_global_calls() {
    // Tier 1: missing/invalid required argument always throws a TypeError.
    // `String.raw(template)` reads `template.raw`; `undefined`/non-template throws.
    test("String.raw()", true);
    test("String.raw({})", true);
    test("String.raw(x)", true);
    // `Symbol.keyFor(sym)` requires a Symbol argument; anything else throws.
    test("Symbol.keyFor()", true);
    test("Symbol.keyFor(42)", true);
    test("Symbol.keyFor(x)", true);
    // `URL.canParse(url)` has a required first argument; with none it throws.
    test("URL.canParse()", true);
    test("URL.canParse('x')", false); // a string argument is fine
    test("URL.canParse(x)", false);

    // Tier 2: `String.fromCodePoint(cp)` throws RangeError unless every `cp` is an
    // integer in [0, 0x10FFFF]. `fromCharCode` truncates via ToUint16 and never
    // RangeErrors.
    test("String.fromCodePoint(-1)", true);
    test("String.fromCodePoint(1.5)", true);
    test("String.fromCodePoint(0x110000)", true);
    test("String.fromCodePoint(65)", false);
    test("String.fromCodePoint(0, 0x10FFFF)", false);
    test("String.fromCodePoint(0, -1)", true);
    test("String.fromCodePoint(x)", false); // undetermined -> stay droppable
    test("String.fromCharCode(-1)", false);
    test("String.fromCharCode(1.5)", false);

    // Tier 3: `ToNumber` coercion throws a TypeError on a BigInt argument (provable).
    // A Symbol also throws, but oxc can never prove a value is a Symbol, so — like
    // esbuild — those stay droppable; only the BigInt cases are guarded.
    test("Math.abs(10n)", true);
    test("Math.floor(10n)", true);
    test("Math.max(1, 10n)", true);
    test("Date.UTC(10n)", true);
    test("String.fromCharCode(10n)", true);
    test("String.fromCodePoint(10n)", true);
    test("isNaN(10n)", true);
    test("isFinite(10n)", true);
    // Non-BigInt arguments remain pure/droppable.
    test("Math.abs(1)", false);
    test("Math.floor(1.5)", false);
    test("Math.max(1, 2)", false);
    test("Date.UTC(2020, 0)", false);
    // `ToString` coercers (parseInt/parseFloat/decodeURI/.../Date.parse/Symbol.for)
    // accept BigInt (`ToString(10n)` -> "10"); only a Symbol throws (not provable).
    test("parseInt(10n)", false);
    test("parseFloat(10n)", false);
    test("decodeURI(10n)", false);
    test("Number.parseInt(10n)", false);
    test("Date.parse(10n)", false);
    test("Symbol.for(10n)", false);
}

#[test]
fn test_call_like_expressions() {
    test("foo()", true);
    test("/* #__PURE__ */ foo()", false);
    test("/* #__PURE__ */ foo(1)", false);
    test("/* #__PURE__ */ foo(bar())", true);
    test("/* #__PURE__ */ foo(...[])", false);
    test("/* #__PURE__ */ foo(...[1])", false);
    test("/* #__PURE__ */ foo(...[bar()])", true);
    test("/* #__PURE__ */ foo(...bar)", true);
    test("/* #__PURE__ */ foo(...`foo`)", false);
    test("/* #__PURE__ */ foo(...`${1}`)", false);
    test("/* #__PURE__ */ foo(...`${bar}`)", true);
    test("/* #__PURE__ */ foo(...`${bar()}`)", true);
    test("/* #__PURE__ */ (() => { foo() })()", false);
    test("foo?.()", true);
    test("/* #__PURE__ */ foo?.()", false);

    test("new Foo()", true);
    test("/* #__PURE__ */ new Foo()", false);
    test("/* #__PURE__ */ new Foo(1)", false);
    test("/* #__PURE__ */ new Foo(bar())", true);
    test("/* #__PURE__ */ new Foo(...[])", false);
    test("/* #__PURE__ */ new Foo(...[1])", false);
    test("/* #__PURE__ */ new Foo(...[bar()])", true);
    test("/* #__PURE__ */ new Foo(...bar)", true);
    test("/* #__PURE__ */ new Foo(...`foo`)", false);
    test("/* #__PURE__ */ new Foo(...`${1}`)", false);
    test("/* #__PURE__ */ new Foo(...`${bar}`)", true);
    test("/* #__PURE__ */ new Foo(...`${bar()}`)", true);
    test("/* #__PURE__ */ new class { constructor() { foo() } }()", false);

    let ctx = Ctx { annotation: false, ..Default::default() };
    test_with_ctx("/* #__PURE__ */ foo()", &ctx, true);
    test_with_ctx("/* #__PURE__ */ new Foo()", &ctx, true);
}

#[test]
fn test_is_pure_call_support() {
    let ctx = Ctx {
        pure_function_names: vec!["foo".to_string(), "Foo".to_string()],
        ..Default::default()
    };
    test_with_ctx("foo()", &ctx, false);
    test_with_ctx("foo(1)", &ctx, false);
    test_with_ctx("foo(bar())", &ctx, true);
    test_with_ctx("bar()", &ctx, true);
    test_with_ctx("new Foo()", &ctx, false);
    test_with_ctx("new Foo(1)", &ctx, false);
    test_with_ctx("new Foo(bar())", &ctx, true);
    test_with_ctx("new Bar()", &ctx, true);
    test_with_ctx("foo``", &ctx, false);
    test_with_ctx("foo`1`", &ctx, false);
    test_with_ctx("foo`${bar()}`", &ctx, true);
    test_with_ctx("bar``", &ctx, true);
}

#[test]
fn test_manual_pure_functions_with_dotted_names() {
    let ctx = Ctx { pure_function_names: vec!["console".to_string()], ..Default::default() };
    test_with_ctx("console()", &ctx, false);
    test_with_ctx("console.log()", &ctx, false);
    test_with_ctx("console.log(bar())", &ctx, true);
    test_with_ctx("other.log()", &ctx, true);
    let ctx = Ctx { pure_function_names: vec!["console.log".to_string()], ..Default::default() };
    test_with_ctx("console.log()", &ctx, false);
    test_with_ctx("console.warn()", &ctx, true);
    test_with_ctx("console.log.foo()", &ctx, false);
}

#[test]
fn test_property_read_side_effects_support() {
    let all_ctx =
        Ctx { property_read_side_effects: PropertyReadSideEffects::All, ..Default::default() };
    let none_ctx =
        Ctx { property_read_side_effects: PropertyReadSideEffects::None, ..Default::default() };

    test_with_ctx("foo.bar", &all_ctx, true);
    test_with_ctx("foo.bar", &none_ctx, false);
    test_with_ctx("foo[0]", &none_ctx, false);
    test_with_ctx("foo[0n]", &none_ctx, false);
    test_with_ctx("foo[bar()]", &none_ctx, true);
    // Non-literal keys: when property_read_side_effects is None,
    // only the key expression and object are checked for side effects
    test_with_ctx("foo[bar]", &all_ctx, true);
    test_with_ctx("foo[bar]", &none_ctx, false);
    test_with_ctx("foo[bar()]", &all_ctx, true);
    test_with_ctx("foo[bar()]", &none_ctx, true); // bar() itself has side effects
    test_with_ctx("foo.#bar", &all_ctx, true);
    test_with_ctx("foo.#bar", &none_ctx, false);
    test_with_ctx("({ bar } = foo)", &all_ctx, true);
    // test_with_ctx("({ bar } = foo)", &none_ctx, false);

    // ObjectExpression spread: when property_read_side_effects is None,
    // spread delegates to the argument's own side effects
    test_with_ctx("({...foo})", &all_ctx, true);
    test_with_ctx("({...foo})", &none_ctx, false);
    test_with_ctx("({...foo()})", &all_ctx, true);
    test_with_ctx("({...foo()})", &none_ctx, true); // foo() itself has side effects
}

#[test]
fn test_unknown_global_side_effects_support() {
    let true_ctx = Ctx {
        unknown_global_side_effects: true,
        global_variable_names: FxHashSet::from_iter(["foo"]),
        ..Default::default()
    };
    let false_ctx = Ctx {
        unknown_global_side_effects: false,
        global_variable_names: FxHashSet::from_iter(["foo"]),
        ..Default::default()
    };
    test_with_ctx("foo", &true_ctx, true);
    test_with_ctx("foo", &false_ctx, false);
}

#[test]
fn test_object_with_to_primitive_related_properties_overridden() {
    test("+{}", false);
    test("+{ foo: 0 }", false);
    test("+{ toString() { return Symbol() } }", true);
    test("+{ valueOf() { return Symbol() } }", true);
    test("+{ 'toString'() { return Symbol() } }", true);
    test("+{ 'valueOf'() { return Symbol() } }", true);
    test("+{ ['toString']() { return Symbol() } }", true);
    test("+{ ['valueOf']() { return Symbol() } }", true);
    test("+{ [`toString`]() { return Symbol() } }", true);
    test("+{ [`valueOf`]() { return Symbol() } }", true);
    test("+{ [Symbol.toPrimitive]() { return Symbol() } }", true);
    test("+{ ...foo }", true); // foo can include toString / valueOf / Symbol.toPrimitive
    test("+{ ...[] }", false);
    test("+{ ...'foo' }", false);
    test("+{ ...`foo` }", false);
    test("+{ ...`foo${1}` }", false);
    test("+{ ...`foo${foo}` }", true);
    test("+{ ...`foo${foo()}` }", true);
    test("+{ ...{ toString() { return Symbol() } } }", true);
    test("+{ ...{ valueOf() { return Symbol() } } }", true);
    test("+{ ...{ [Symbol.toPrimitive]() { return Symbol() } } }", true);
}

#[test]
fn test_typeof_guard_patterns() {
    test_with_global_variables("typeof x !== 'undefined' && x", &["x"], false);
    test_with_global_variables("typeof x != 'undefined' && x", &["x"], false);
    test_with_global_variables("'undefined' !== typeof x && x", &["x"], false);
    test_with_global_variables("'undefined' != typeof x && x", &["x"], false);
    test_with_global_variables("typeof x === 'undefined' || x", &["x"], false);
    test_with_global_variables("typeof x == 'undefined' || x", &["x"], false);
    test_with_global_variables("'undefined' === typeof x || x", &["x"], false);
    test_with_global_variables("'undefined' == typeof x || x", &["x"], false);
    test_with_global_variables("typeof x < 'u' && x", &["x"], false);
    test_with_global_variables("typeof x <= 'u' && x", &["x"], false);
    test_with_global_variables("'u' > typeof x && x", &["x"], false);
    test_with_global_variables("'u' >= typeof x && x", &["x"], false);
    test_with_global_variables("typeof x > 'u' || x", &["x"], false);
    test_with_global_variables("typeof x >= 'u' || x", &["x"], false);
    test_with_global_variables("'u' < typeof x || x", &["x"], false);
    test_with_global_variables("'u' <= typeof x || x", &["x"], false);

    test_with_global_variables("typeof x === 'undefined' ? 0 : x", &["x"], false);
    test_with_global_variables("typeof x == 'undefined' ? 0 : x", &["x"], false);
    test_with_global_variables("'undefined' === typeof x ? 0 : x", &["x"], false);
    test_with_global_variables("'undefined' == typeof x ? 0 : x", &["x"], false);
    test_with_global_variables("typeof x !== 'undefined' ? x : 0", &["x"], false);
    test_with_global_variables("typeof x != 'undefined' ? x : 0", &["x"], false);
    test_with_global_variables("'undefined' !== typeof x ? x : 0", &["x"], false);
    test_with_global_variables("'undefined' != typeof x ? x : 0", &["x"], false);

    test_with_global_variables("typeof x !== 'undefined' && (x + foo())", &["x"], true);
    test_with_global_variables("typeof x === 'undefined' || (x + foo())", &["x"], true);
    test_with_global_variables("typeof x === 'undefined' ? foo() : x", &["x"], true);
    test_with_global_variables("typeof x !== 'undefined' ? x : foo()", &["x"], true);
    test_with_global_variables("typeof foo() !== 'undefined' && x", &["x"], true);
    test_with_global_variables("typeof foo() === 'undefined' || x", &["x"], true);
    test_with_global_variables("typeof foo() === 'undefined' ? 0 : x", &["x"], true);
    test_with_global_variables("typeof y !== 'undefined' && x", &["x", "y"], true);
    test_with_global_variables("typeof y === 'undefined' || x", &["x", "y"], true);
    test_with_global_variables("typeof y === 'undefined' ? 0 : x", &["x", "y"], true);

    test("typeof localVar !== 'undefined' && localVar", false);
    test("typeof localVar === 'undefined' || localVar", false);
    test("typeof localVar === 'undefined' ? 0 : localVar", false);

    test_with_global_variables(
        "typeof x !== 'undefined' && typeof y !== 'undefined' && x && y",
        &["x", "y"],
        true, // This can be improved
    );
}

#[test]
fn test_assignment_targets() {
    test_assign_target("a = 1", false);
    test_assign_target("String = 1", false);
    test_assign_target("({ a } = 1)", true); // this can be improved
    test_assign_target("([a] = 1)", true); // this can be improved
    test_assign_target("a.b = 1", false); // the side effect of the setter of `a.b` happens in `PutValue`
    test_assign_target_with_global_variables("a.b = 1", &["a"], true); // `a` might not be declared and cause ReferenceError in strict mode
    test_assign_target("(foo(), a).b = 1", true); // `foo()` may have sideeffect
    test_assign_target("a['b'] = 1", false);
    test_assign_target("a[foo()] = 1", true); // `foo()` may have sideeffect
    test_assign_target_with_global_variables("a['b'] = 1", &["a"], true); // `a` might not be declared and cause ReferenceError in strict mode
    test_assign_target("a.#b = 1", false);
    test_assign_target_with_global_variables("a.#b = 1", &["a"], true); // `a` might not be declared and cause ReferenceError in strict mode
    test_assign_target("(foo(), a).#b = 1", true); // `foo()` may have sideeffect
}

#[test]
fn test_property_write_side_effects_support() {
    // With property_write_side_effects: true (default), all writes have side effects
    let write_ctx = Ctx { property_write_side_effects: true, ..Default::default() };
    test_with_ctx("a.b = 1", &write_ctx, true);
    test_with_ctx("a.b += 1", &write_ctx, true);
    test_with_ctx("a.b++", &write_ctx, true);

    // With property_write_side_effects: false, simple assignment is side-effect-free
    let no_write_ctx = Ctx {
        property_write_side_effects: false,
        property_read_side_effects: PropertyReadSideEffects::All,
        ..Default::default()
    };
    test_with_ctx("a.b = 1", &no_write_ctx, false); // simple assign, no read
    test_with_ctx("a['b'] = 1", &no_write_ctx, false);
    test_with_ctx("a.#b = 1", &no_write_ctx, false);

    // Compound assignments have an implicit read — still side-effectful when reads have side effects
    test_with_ctx("a.b += 1", &no_write_ctx, true);
    test_with_ctx("a.b -= 1", &no_write_ctx, true);
    test_with_ctx("a.b &&= 1", &no_write_ctx, true);
    test_with_ctx("a['b'] += 1", &no_write_ctx, true);
    test_with_ctx("a.#b += 1", &no_write_ctx, true);

    // Update expressions have an implicit read — side-effectful when reads have side effects
    test_with_ctx("a.b++", &no_write_ctx, true);
    test_with_ctx("a.b--", &no_write_ctx, true);
    test_with_ctx("++a.b", &no_write_ctx, true);
    test_with_ctx("a['b']++", &no_write_ctx, true);
    test_with_ctx("a.#b++", &no_write_ctx, true);

    // Compound assignments and updates always have side effects due to implicit coercions
    // (ToPrimitive/ToNumeric), even with both write and read side effects off
    let no_side_effects_ctx = Ctx {
        property_write_side_effects: false,
        property_read_side_effects: PropertyReadSideEffects::None,
        ..Default::default()
    };
    test_with_ctx("a.b = 1", &no_side_effects_ctx, false); // simple assign is free
    test_with_ctx("a.b += 1", &no_side_effects_ctx, true); // compound: ToNumeric coercion
    test_with_ctx("a.b++", &no_side_effects_ctx, true); // update: ToNumeric coercion
    test_with_ctx("a['b'] += 1", &no_side_effects_ctx, true);
    test_with_ctx("a['b']++", &no_side_effects_ctx, true);
    test_with_ctx("a.#b += 1", &no_side_effects_ctx, true);
    test_with_ctx("a.#b++", &no_side_effects_ctx, true);

    // Sub-expression side effects still propagate
    test_with_ctx("(foo()).b = 1", &no_side_effects_ctx, true);
    test_with_ctx("a[foo()] = 1", &no_side_effects_ctx, true);
}
