/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.classgen.asm.sc

import groovy.transform.stc.FieldsAndPropertiesSTCTest

/**
 * Unit tests for static compilation : fields and properties.
 */
final class FieldsAndPropertiesStaticCompileTest extends FieldsAndPropertiesSTCTest implements StaticCompilationTestSupport {

    // GROOVY-5561
    void testShouldNotThrowAccessForbidden() {
        assertScript '''
            class Test {
                def foo() {
                    def bar = createBar()
                    bar.foo
                }
                Bar createBar() { new Bar() }
            }
            class Bar {
                List<String> foo = ['1','2']
            }
            assert new Test().foo() == ['1','2']
        '''
    }

    // GROOVY-5579
    void testUseSetterNotSetProperty() {
        assertScript '''
            Date d = new Date()
            d.time = 1

            assert d.time == 1
        '''
        assert astTrees.values().any {
            it.toString().contains 'INVOKEVIRTUAL java/util/Date.setTime (J)V'
        }
    }

    void testUseDirectWriteFieldAccess() {
        assertScript '''
            class C {
                boolean setterCalled

                protected int x
                public void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            class D extends C {
                void directAccess() {
                    this.@x = 2
                }
            }
            D d = new D()
            d.directAccess()
            assert d.isSetterCalled() == false
            assert d.x == 2
        '''
        assert astTrees['D'][1].contains('PUTFIELD C.x')
    }

    void testUseDirectWriteStaticFieldAccess() {
        assertScript '''
            class C {
                static boolean setterCalled

                static protected int x
                public static void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            class D extends C {
                static void directAccess() {
                    this.@x = 2
                }
            }
            D.directAccess()
            assert D.isSetterCalled() == false
            assert D.x == 2
        '''
        assert astTrees['D'][1].contains('PUTSTATIC C.x')
    }

    void testUseSetterFieldAccess() {
        assertScript '''
            class C {
                boolean setterCalled

                protected int x
                public void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            class D extends C {
                void setterAccess() {
                    this.x = 2
                }
            }
            D d = new D()
            d.setterAccess()
            assert d.isSetterCalled() == true
            assert d.x == 2
        '''
        assert astTrees['D'][1].contains('INVOKEVIRTUAL D.setX')
    }

    void testUseDirectWriteFieldAccessFromOutsideClass() {
        assertScript '''
            class C {
                public int x
            }
            class D  {
                void directAccess(C c) {
                    c.@x = 2
                }
            }
            D d = new D()
            C c = new C()
            d.directAccess(c)
            assert c.x == 2
        '''
        assert astTrees['D'][1].contains('PUTFIELD C.x')
    }

    void testUseDirectWriteFieldAccessPrivateWithRuntimeClassBeingDifferent() {
        assertScript '''
            class C {
                private int x
                public C(int x) {
                    this.@x = x
                }
                public boolean sameAs(C c) {
                    return this.@x == c.@x
                }
            }
            class D extends C {
                // D.x visible in D C.x in C, but reflection depending on the runtime type would see C.x in C#sameAs and not C.x
                private int x
                public D(int x) {
                    super(x)
                    this.@x = x + 50
                }
            }
            D d = new D(1)
            C c = new C(1)
            assert d.sameAs(c)
        '''
        // same with property style access:
        assertScript '''
            class C {
                private int x
                public C(int x) {
                    this.x = x
                }
                public boolean sameAs(C c) {
                    return this.x == c.x
                }
            }
            class D extends C {
                // D.x visible in D C.x in C, but reflection depending on the runtime type would see D.x in C#sameAs and not C.x
                private int x
                public D(int x) {
                    super(x)
                    this.x = x + 50
                }
            }
            D d = new D(1)
            C c = new C(1)
            assert d.sameAs(c)
        '''
    }

    void testReadFieldFromSameClass() {
        ['', 'public', 'private', 'protected', '@groovy.transform.PackageScope'].each { mod ->
            assertScript """
                class C {
                    $mod int x
                    int m() {
                        x
                    }
                }
                assert new C().m() == 0
            """
            def a = astTrees['C'][1]
            assert (a =~ 'GETFIELD C.x').collect().size() == mod.empty ? 2 : 1
        }
    }

    void testWriteFieldFromSameClass() {
        ['', 'public', 'private', 'protected', '@groovy.transform.PackageScope'].each { mod ->
            assertScript """
                class C {
                    $mod int x
                    int m() {
                        x = 5
                        x
                    }
                }
                new C().m() == 5
            """
            def a = astTrees['C'][1]
            assert (a =~ 'PUTFIELD C.x').collect().size() == mod.empty ? 2 : 1
        }
    }

    void testReadFieldFromSuperClass() {
        ['public', 'protected', '@groovy.transform.PackageScope'].each { mod ->
            assertScript """
                class C {
                    $mod int x
                }
                class D extends C {
                    int m() {
                        x
                    }
                }
                assert new D().m() == 0
            """
            def d = astTrees['D'][1]
            assert d.contains('GETFIELD C.x')
        }
    }

    // GROOVY-9791
    void testReadFieldFromSuperClass2() {
        assertScript '''
            package p
            class C {
                protected int x
            }
            new p.C()
        '''
        assertScript '''
            class D extends p.C {
                int m() {
                    x
                }
            }
            assert new D().m() == 0
        '''
        def b = astTrees['D'][1]
        assert  b.contains('GETFIELD p/C.x')
        assert !b.contains('INVOKEINTERFACE groovy/lang/GroovyObject.getProperty')
    }

    // GROOVY-9791
    void testReadFieldFromSuperClass3() {
        assertScript '''
            package p
            class C {
                protected static int x
            }
            new p.C()
        '''
        assertScript '''
            class D extends p.C {
                static int m() {
                    x
                }
            }
            assert D.m() == 0
        '''
        def d = astTrees['D'][1]
        assert  d.contains('GETSTATIC D.x')
        assert !d.contains('INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.getGroovyObjectProperty')
    }

    void testReadPropertyFromSuperClass() {
        ['', 'public', 'private', 'protected', '@groovy.transform.PackageScope'].each { mod ->
            assertScript """
                class C {
                    $mod int x
                    int getX() { x }
                }
                class D extends C {
                    int m() {
                        x
                    }
                }
                assert new D().m() == 0
            """
            def d = astTrees['D'][1]
            assert !d.contains('GETFIELD C.x') : 'no GETFIELD in D'
            assert  d.contains('INVOKEVIRTUAL D.getX') : 'getX() in D'
        }
    }

    void testUseDirectReadFieldAccess() {
        assertScript '''
            class C {
                boolean getterCalled

                protected int x
                public int getX() {
                    getterCalled = true
                    x
                }
            }
            class D extends C {
                void m() {
                    this.@x
                }
            }
            D d = new D()
            d.m()
            assert d.isGetterCalled() == false
        '''
        def d = astTrees['D'][1]
        assert d.contains('GETFIELD C.x')
    }

    void testUseAttributeExternal() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            C c = new C()
            c.@x = 100
            assert c.x == 100
            assert c.isSetterCalled() == false
        '''
    }

    void testUseAttributeExternalSafe() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            C c = new C()
            c?.@x = 100
            assert c.x == 100
            assert c.isSetterCalled() == false
        '''
    }

    void testUseAttributeExternalSafeWithNull() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            C c = null
            c?.@x = 100
        '''
    }

    void testUseSetterExternal() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            C c = new C()
            c.x = 100
            assert c.x == 100
            assert c.isSetterCalled() == true
        '''
    }

    void testUseAttributeExternalSpread() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            List<C> list = [new C(), new C()]
            list*.@x = 100
            assert list[0].x == 100
            assert list[0].isSetterCalled() == false
        '''
    }

    void testUseAttributeExternalSpreadSafeWithNull() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            List<C> list = [new C(), null]
            list*.@x = 100
            assert list[0].x == 100
            assert list[0].isSetterCalled() == false
            assert list[1] == null
        '''
    }

    void testUseAttributeExternalSpreadUsingSetter() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            List<C> list = [new C(), new C()]
            list*.x = 100
            assert list[0].x == 100
            assert list[0].isSetterCalled() == true
        '''
    }

    void testUseAttributeExternalSpreadSafeWithNullUsingSetter() {
        assertScript '''
            class C {
                boolean setterCalled

                public int x
                void setX(int a) {
                    setterCalled = true
                    x = a
                }
            }
            List<C> list = [new C(), null]
            list*.x = 100
            assert list[0].x == 100
            assert list[0].isSetterCalled() == true
            assert list[1] == null
        '''
    }

    // GROOVY-5649
    void testShouldNotThrowStackOverflowUsingThis() {
        new GroovyShell().evaluate '''
            class HaveOption {
                private String helpOption

                public void setHelpOption(String helpOption) {
                    this.helpOption = helpOption
                }
            }
            def o = new HaveOption()
            o.setHelpOption 'foo'
            assert o.helpOption
        '''
    }

    void testShouldNotThrowStackOverflow() {
        new GroovyShell().evaluate '''
            class HaveOption {
              private String helpOption

              public void setHelpOption(String ho) {
                  helpOption = ho
              }
            }
            def o = new HaveOption()
            o.setHelpOption 'foo'
            assert o.helpOption
        '''
    }

    @Override
    void testPropertyWithMultipleSetters() {
        // we need to override the test because the AST is going to be changed
        assertScript '''
            import org.codehaus.groovy.ast.expr.*
            import org.codehaus.groovy.ast.stmt.*
            import org.codehaus.groovy.transform.sc.ListOfExpressionsExpression

            class C {
                private field
                void setX(Integer a) {field=a}
                void setX(String b) {field=b}
                def getX(){field}
            }

            @ASTTest(phase=INSTRUCTION_SELECTION, value={
                lookup('test1').each { stmt ->
                    def exp = stmt.expression
                    assert exp instanceof ListOfExpressionsExpression
                }
                lookup('test2').each { stmt ->
                    def exp = stmt.expression
                    assert exp instanceof ListOfExpressionsExpression
                }
            })
            void test() {
                def c = new C()
                test1:
                c.x = 1
                assert c.x==1
                test2:
                c.x = "3"
                assert c.x == "3"
            }
            test()
        '''
    }

    void testCallSetterAsPropertyWithinFinallyBlockShouldNotThrowVerifyError() {
        try {
            assertScript '''
                class C {
                   void setOut(int a) {}
                }
                def c = new C()
                try {
                } finally {
                    c.out = 1
                }
            '''
        } finally {
            assert astTrees.values().any {
                it.toString().contains 'INVOKEVIRTUAL C.setOut (I)V'
            }
        }
    }

    void testCallMultiSetterAsPropertyWithinFinallyBlockShouldNotThrowVerifyError() {
        try {
            assertScript '''
                class C {
                   void setOut(int a) {}
                   void setOut(String a) {}
                }
                def c = new C()
                try {
                } finally {
                    c.out = 1
                    c.out = 'foo'
                }
            '''
        } finally {
            assert astTrees.values().any {
                def code = it.toString()
                code.contains('INVOKEVIRTUAL C.setOut (I)V')
                        && code.contains('INVOKEVIRTUAL C.setOut (Ljava/lang/String;)V')
            }
        }
    }

    // GROOVY-7698
    void testSafePropertyStyleSetterCalls() {
        assertScript '''
            class C {
                private String id

                void setId(String id) {
                    this.id = id
                }
            }
            C c = null
            c?.id = 'new'
        '''
    }

    // GROOVY-9700
    void testVariableAssignmentUsesDirectSetterCall() {
        assertScript '''
            import org.codehaus.groovy.transform.sc.ListOfExpressionsExpression

            class Foo {
                void setX(Date value) {}
                void setX(Long value) {}
            }
            class Bar extends Foo {
                @ASTTest(phase=INSTRUCTION_SELECTION, value={
                    def assignment = node.code.statements[0].expression
                    assert assignment instanceof ListOfExpressionsExpression
                        assignment = node.code.statements[1].expression
                    assert assignment instanceof ListOfExpressionsExpression
                })
                void test() {
                    x = 42L
                    x = new Date()
                }
            }
            new Bar().test()
        '''

        def bar = astTrees['Bar'][1]
        assert bar.contains('INVOKEVIRTUAL Bar.setX (Ljava/lang/Long;)V')
        assert bar.contains('INVOKEVIRTUAL Bar.setX (Ljava/util/Date;)V')
        assert !bar.contains('INVOKESTATIC org/codehaus/groovy/runtime/ScriptBytecodeAdapter.setGroovyObjectProperty ')
    }

    void testPrivateFieldMutationInClosureUsesBridgeMethod() {
        try {
            assertScript '''
                class Foo {
                    private String s
                    Closure c = { this.s = 'abc' }

                    void test() {
                        c()
                        assert s == 'abc'
                    }
                }
                new Foo().test()
            '''
        } finally {
            assert astTrees['Foo$_closure1'][1].contains('INVOKESTATIC Foo.pfaccess$00 (LFoo;Ljava/lang/String;)Ljava/lang/String')
        }
    }

    void testImplicitPrivateFieldMutationInClosureUsesBridgeMethod() {
        try {
            assertScript '''
                class Foo {
                    private String s
                    Closure c = { s = 'abc' }

                    String test() {
                        c()
                        assert s == 'abc'
                    }
                }
                new Foo().test()
            '''
        } finally {
            assert astTrees['Foo$_closure1'][1].contains('INVOKESTATIC Foo.pfaccess$00 (LFoo;Ljava/lang/String;)Ljava/lang/String')
        }
    }

    void testPrivateStaticFieldMutationInClosureUsesBridgeMethod() {
        try {
            assertScript '''
                class Foo {
                    private static String s
                    Closure c = { s = 'abc' }

                    String test() {
                        c()
                        assert s == 'abc'
                    }
                }
                new Foo().test()
            '''
        } finally {
            assert astTrees['Foo$_closure1'][1].contains('INVOKESTATIC Foo.pfaccess$00 (LFoo;Ljava/lang/String;)Ljava/lang/String')
        }
    }

    void testPrivateFieldMutationInAICUsesBridgeMethod() {
        try {
            assertScript '''
                class C {
                    private int x
                    void test() {
                        def aic = new Runnable() { void run() { C.this.x = 666 } }
                        aic.run()
                        assert x == 666
                    }
                }
                new C().test()
            '''
        } finally {
            assert astTrees['C$1'][1].contains('INVOKESTATIC C.pfaccess$00 (LC;I)I')
        }
    }

    void testImplicitPrivateFieldMutationInAICUsesBridgeMethod() {
        try {
            assertScript '''
                class C {
                    private int x
                    void test() {
                        def aic = new Runnable() { void run() { x = 666 } }
                        aic.run()
                        assert x == 666
                    }
                }
                new C().test()
            '''
        } finally {
            assert astTrees['C$1'][1].contains('INVOKESTATIC C.pfaccess$00 (LC;I)I')
        }
    }

    void testPrivateStaticFieldMutationInAICUsesBridgeMethod() {
        try {
            assertScript '''
                class C {
                    private static int x
                    void test() {
                        def aic = new Runnable() { void run() { x = 666 } }
                        aic.run()
                        assert x == 666
                    }
                }
                new C().test()
            '''
        } finally {
            assert astTrees['C$1'][1].contains('INVOKESTATIC C.pfaccess$00 (LC;I)I')
        }
    }

    void testMultiplePrivateFieldMutatorBridgeMethods() {
        try {
            assertScript '''
                class C {
                    private int x
                    private String y
                    Closure mutate = { x = 1; y = 'abc' }

                    void test() {
                        mutate()
                        assert x == 1
                        assert y == 'abc'
                    }
                }
                new C().test()
            '''
        } finally {
            assert astTrees['C$_closure1'][1].contains('INVOKESTATIC C.pfaccess$00 (LC;I)I')
            assert astTrees['C$_closure1'][1].contains('INVOKESTATIC C.pfaccess$01 (LC;Ljava/lang/String;)Ljava/lang/String;')
        }
    }

    void testPrivateFieldBridgeMethodsAreGeneratedAsNecessary() {
        try {
            assertScript '''
                class C {
                    private int accessed = 0
                    private String mutated
                    private String accessedAndMutated = ''
                    Closure cl = {
                        println accessed
                        mutated = 'abc'
                        println accessedAndMutated
                        accessedAndMutated = 'def'
                    }
                    void test() {
                        cl()
                        assert mutated == 'abc'
                        assert accessedAndMutated == 'def'
                    }
                }
                new C().test()
            '''
        } finally {
            def dump = astTrees['C'][1]
            assert dump.contains('pfaccess$0') // accessor bridge method for 'accessed'
            assert !dump.contains('pfaccess$00') // no mutator bridge method for 'accessed'
            assert dump.contains('pfaccess$01') // mutator bridge method for 'mutated'
            assert dump.contains('pfaccess$1') // accessor bridge method for 'mutated' -- GROOVY-9385
            assert dump.contains('pfaccess$2') // accessor bridge method for 'accessedAndMutated'
            assert dump.contains('pfaccess$02') // mutator bridge method for 'accessedAndMutated'
            dump = astTrees['C$_closure1'][1]
            assert dump.contains('INVOKESTATIC C.pfaccess$2 (LC;)Ljava/lang/String;')
            assert dump.contains('INVOKESTATIC C.pfaccess$02 (LC;Ljava/lang/String;)Ljava/lang/String;')
        }
    }

    // GROOVY-8369
    void testPropertyAccessOnEnumClass() {
        assertScript '''
            enum E { }
            assert E.modifiers == E.getModifiers()
        '''
    }

    // GROOVY-8753
    void testPrivateFieldWithPublicGetter() {
        assertScript '''
            class C {
               private List<String> fooNames = []
               public C(Collection<String> names) {
                  names.each { fooNames << it }
               }
               public List<String> getFooNames() { fooNames }
            }
            assert new C(['foo1', 'foo2']).fooNames.size() == 2
        '''
    }

    // GROOVY-11369
    @Override
    void testMapPropertyAccess5a() {
        for (mod in ['def', 'public', 'protected', '@groovy.transform.PackageScope', 'private']) {
            assertScript """
                class C implements Map<String,String> {
                    @Delegate Map<String,String> impl = [:].withDefault{'entry'}
                    $mod getFoo() { 'getter' }
                    void test() {
                        @ASTTest(phase=INSTRUCTION_SELECTION, value={
                            assert node.getNodeMetaData(INFERRED_TYPE) == OBJECT_TYPE
                        })
                        def which = this.foo
                        assert which == 'getter'
                    }
                }
                new C().test()
            """
        }
        assertScript '''
            class C implements Map<String,String> {
                @Delegate Map<String,String> impl = [:].withDefault{ 'entry' }
            }
            class D extends C {
                void test() {
                    @ASTTest(phase=INSTRUCTION_SELECTION, value={
                        assert node.getNodeMetaData(INFERRED_TYPE) == METACLASS_TYPE
                    })
                    def one = this.metaClass; assert one instanceof MetaClass

                    @ASTTest(phase=INSTRUCTION_SELECTION, value={
                        assert node.getNodeMetaData(INFERRED_TYPE) == STRING_TYPE
                    })
                    def two = super.metaClass; assert two == 'entry'
                }
            }
            new D().test()
        '''
    }

    // GROOVY-11223, GROOVY-11373
    @Override
    void testMapPropertyAccess10() {
        assertScript """
            def map = new ${MapType.name}()
            map.foo = 11 // public setter
            assert map.foo == null
            assert map.getFoo() == 11
        """
        shouldFailWithMessages """
            def map = new ${MapType.name}()
            map.bar = 22 // protected setter
        """,
        "Cannot access method: setBar(java.lang.Object) of class: ${MapType.name}"

        shouldFailWithMessages """
            def map = new ${MapType.name}()
            map.baz = 33 // package-private setter
        """,
        "Cannot access method: setBaz(java.lang.Object) of class: ${MapType.name}"
    }
}
