/*
 *  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.transform

import gls.CompilableTestSupport

/**
 * Tests for the {@code @Delegate} AST transform.
 */
class DelegateTransformTest extends CompilableTestSupport {

    /** fix for GROOVY-3380   */
    void testDelegateImplementingANonPublicInterface() {
        assertScript """
            import org.codehaus.groovy.transform.ClassImplementingANonPublicInterface

            class DelegatingToClassImplementingANonPublicInterface {
                @Delegate ClassImplementingANonPublicInterface delegate = new ClassImplementingANonPublicInterface()
            }

            def constant = new DelegatingToClassImplementingANonPublicInterface().returnConstant()
            assert constant == "constant"
        """
    }

    /** fix for GROOVY-3380   */
    void testDelegateImplementingANonPublicInterfaceWithZipFileConcreteCase() {
        assertScript """
            import java.util.zip.*

            class ZipWrapper{
               @Delegate ZipFile zipFile
            }

            new ZipWrapper()
        """
    }

    /** test for GROOVY-5974 */
    void testDelegateExcludes() {
        assertScript """
          class MapSet {
            @Delegate(interfaces=false, excludes=['remove','clear']) Map m = [a: 1]
            @Delegate Set s = new LinkedHashSet([2, 3, 4] as Set) // HashSet not good enough in JDK 1.5
            String toString() { m.toString() + ' ' + s }
          }

          def ms = new MapSet()
          assert ms.size() == 1
          assert ms.toString() == '[a:1] [2, 3, 4]'
          ms.remove(3)
          assert ms.size() == 1
          assert ms.toString() == '[a:1] [2, 4]'
          ms.clear()
          assert ms.toString() == '[a:1] []'
        """
    }

    void testDelegateCompileStatic() {
        assertScript """
          @groovy.transform.CompileStatic
          class MapSet {
            @Delegate(interfaces=false, excludes=['remove','clear']) Map m = [a: 1]
            @Delegate Set s = new LinkedHashSet([2, 3, 4] as Set)
            String toString() { m.toString() + ' ' + s }
          }

          def ms = new MapSet()
          assert ms.size() == 1
          assert ms.toString() == '{a=1} [2, 3, 4]'
          ms.remove(3)
          assert ms.size() == 1
          assert ms.toString() == '{a=1} [2, 4]'
          ms.clear()
          assert ms.toString() == '{a=1} []'
        """
    }

    void testLock() {
        def res = new GroovyShell().evaluate("""
              import java.util.concurrent.locks.*

              class LockableMap {
                 @Delegate private Map map = [:]

                 @Delegate private Lock lock = new ReentrantLock ()

                 @Delegate(interfaces=false) private List list = new ArrayList ()
              }

              new LockableMap ()
        """)

        res.lock()
        try {
            res[0] = 0
            res[1] = 1
            res[2] = 2

            res.add("in list")
        }
        finally {
            res.unlock()
        }

        assertEquals([0: 0, 1: 1, 2: 2], res.@map)
        assertEquals("in list", res.@list[0])

        assertTrue res instanceof Map
        assertTrue res instanceof java.util.concurrent.locks.Lock
        assertFalse res instanceof List
    }

    void testMultiple() {
        def res = new GroovyShell().evaluate("""
        class X {
          def value = 10
        }

        class Y {
          @Delegate X  x  = new X ()
          @Delegate XX xx = new XX ()

          void setValue (v) {
            this.@x.@value = 12
          }
        }

        class XX {
          def value2 = 11
        }

        new Y ()
        """)

        assertEquals 10, res.value
        assertEquals 11, res.value2
        res.value = 123
        assertEquals 12, res.value
    }

    void testUsingDateCompiles() {
        assertScript """
        class Foo { 
          @Delegate Date d = new Date(); 
        } 
        Foo
      """
    }

    /** fix for GROOVY-3471   */
    void testDelegateOnAMapTypeFieldWithInitializationUsingConstructorProperties() {
        assertScript """
            class Test3471 { @Delegate Map mp }
            def t = new Test3471(mp: new HashMap()) // this was resulting in a NPE due to MetaClassImpl's special handling of Map
            assert t.keySet().size() == 0
        """
    }

    /** GROOVY-3323   */
    void testDelegateTransformCorrectlyDelegatesMethodsFromSuperInterfaces() {
        assert new DelegateBarImpl(new DelegateFooImpl()).bar() == 'bar impl'
        assert new DelegateBarImpl(new DelegateFooImpl()).foo() == 'foo impl'
    }

    /** GROOVY-3555   */
    void testDelegateTransformIgnoresDeprecatedMethodsByDefault() {
        def b1 = new DelegateBarForcingDeprecated(baz: new BazWithDeprecatedFoo())
        def b2 = new DelegateBarWithoutDeprecated(baz: new BazWithDeprecatedFoo())
        assert b1.bar() == 'bar'
        assert b2.bar() == 'bar'
        assert b1.foo() == 'foo'
        shouldFail(MissingMethodException) {
            assert b2.foo() == 'foo'
        }
    }

    /** GROOVY-4163   */
    void testDelegateTransformAllowsInterfacesAndDelegation() {
        assertScript """
            class Temp implements Runnable {
                @Delegate
                private Thread runnable

                static main(args) {
                    def thread = Thread.currentThread()
                    def temp = new Temp(runnable: thread)
                }
            }
        """
    }

    void testDelegateToSelfTypeShouldFail() {
        shouldNotCompile """
            class B {
                @Delegate B b = new B()
                static main(args){
                    new B()
                }
            }
        """
    }

    // GROOVY-4265
    void testShouldPreferDelegatedOverStaticSuperMethod() {
        assertScript """
            class A {
                static foo(){"A->foo()"}
            }
            class B extends A {
                @Delegate C c = new C()
            }
            class C {
                def foo(){"C->foo()"}
            }
            assert new B().foo() == 'C->foo()'
        """
    }

    void testDelegateToObjectShouldFail() {
        shouldNotCompile """
            class B {
                @Delegate b = new Object()
            }
        """
    }

    /** GROOVY-4244 */
    void testSetPropertiesThroughDelegate() {
        def foo = new Foo4244()

        assert foo.nonFinalBaz == 'Initial value - nonFinalBaz'
        foo.nonFinalBaz = 'New value - nonFinalBaz'
        assert foo.nonFinalBaz == 'New value - nonFinalBaz'

        assert foo.finalBaz == 'Initial value - finalBaz'
        shouldFail(ReadOnlyPropertyException) {
            foo.finalBaz = 'New value - finalBaz'
        }
    }

    void testDelegateSuperInterfaces_Groovy4619() {
        assert 'doSomething' in SomeClass4619.class.methods*.name
    }

    // GROOVY-5112
    void testGenericsOnArray() {
        assertScript '''
            class ListWrapper {
              @Delegate
              List myList

              @Delegate
              URL homepage
            }
            new ListWrapper()
        '''
    }

    // GROOVY-5732
    void testInterfacesFromSuperClasses() {
        assertScript '''
            interface I5732 {
                void aMethod()
            }

            abstract class AbstractBaseClass implements I5732 { }

            abstract class DelegatedClass extends AbstractBaseClass {
                void aMethod() {}
            }

            class Delegator {
                @Delegate private DelegatedClass delegate
            }

            assert I5732.isAssignableFrom(Delegator)
        '''
    }

    // GROOVY-5729
    void testDeprecationWithInterfaces() {
        assertScript '''
            interface I5729 {
                @Deprecated
                void aMethod()
            }

            class Delegator1 {
                @Delegate private I5729 delegate
            }
            assert I5729.isAssignableFrom(Delegator1)
            assert Delegator1.methods*.name.contains('aMethod')

            class Delegator2 {
                @Delegate(interfaces=false) private I5729 delegate
            }
            assert !I5729.isAssignableFrom(Delegator2)
            assert !Delegator2.methods*.name.contains('aMethod')

            class Delegator3 {
                @Delegate(interfaces=false, deprecated=true) private I5729 delegate
            }
            assert !I5729.isAssignableFrom(Delegator3)
            assert Delegator3.methods*.name.contains('aMethod')
        '''
    }

    // GROOVY-5446
    void testDelegateWithParameterAnnotations() {
        assertScript """
            import java.lang.annotation.*

            @Retention(RetentionPolicy.RUNTIME)
            @Target([ElementType.PARAMETER])
            public @interface SomeAnnotation {
            }

            class A {
                def method(@SomeAnnotation def param) { "Test" }
            }

            class A_Delegate {
                @Delegate(parameterAnnotations = true)
                A a = new A()
            }

            def originalMethod = A.getMethod('method', [Object.class] as Class[])
            def originalAnno = originalMethod.parameterAnnotations[0][0]

            def delegateMethod = A_Delegate.getMethod('method', [Object.class] as Class[])
            def delegateAnno = delegateMethod.parameterAnnotations[0][0]
            println delegateMethod.parameterAnnotations

            assert delegateAnno == originalAnno
        """
    }

    void testDelegateWithMethodAnnotations() {
        assertScript """
            import java.lang.annotation.*

            @Retention(RetentionPolicy.RUNTIME)
            @Target([ElementType.METHOD])
            public @interface SomeAnnotation {
                int value()
            }

            class A {
                @SomeAnnotation(42)
                def method( def param) { "Test" }
            }

            class A_Delegate {
                @Delegate(methodAnnotations = true)
                A a = new A()
            }

            def originalMethod = A.getMethod('method', [Object.class] as Class[])
            def originalAnno = originalMethod.declaredAnnotations[0]

            def delegateMethod = A_Delegate.getMethod('method', [Object.class] as Class[])
            def delegateAnno = delegateMethod.declaredAnnotations[1]

            assert delegateAnno == originalAnno

            assert delegateAnno.value() == 42
            assert delegateAnno.value() == originalAnno.value()
        """
    }

    void testParameterAnnotationsShouldNotBeCarriedOverByDefault() {
        assertScript """
            import java.lang.annotation.*

            @Retention(RetentionPolicy.RUNTIME)
            @Target([ElementType.PARAMETER])
            public @interface SomeAnnotation {
            }

            class A {
                def method(@SomeAnnotation def param) { "Test" }
            }

            class A_Delegate {
                @Delegate
                A a = new A()
            }

            def originalMethod = A.getMethod('method', [Object.class] as Class[])
            def originalAnno = originalMethod.parameterAnnotations[0][0]

            def delegateMethod = A_Delegate.getMethod('method', [Object.class] as Class[])
            assert delegateMethod.parameterAnnotations[0].length == 0
        """
    }

    // this test reflects that we currently don't support carrying over
    // Closure Annotations rather than a desired design goal
    // TODO: support Closure Annotations and then remove/change this test
    void testAnnotationWithClosureMemberIsNotSupported() {
        def message = shouldFail {
            assertScript """
                import java.lang.annotation.*

                @Retention(RetentionPolicy.RUNTIME)
                @Target([ElementType.METHOD])
                public @interface SomeAnnotation {
                    Class value()
                }

                class A {
                    @SomeAnnotation({ param != null })
                    def method(def param) { "Test" }
                }

                class A_Delegate {
                    @Delegate(methodAnnotations = true)
                    A a = new A()
                }
            """
        }

        assert message.contains('@Delegate does not support keeping Closure annotation members.')
    }

    // this test reflects that we currently don't support carrying over
    // Closure Annotations rather than a desired design goal
    // TODO: support Closure Annotations and then remove/change this test
    void testAnnotationWithClosureClassDescendantIsNotSupported() {
        def message = shouldFail {
            assertScript """
                import java.lang.annotation.*

                @Retention(RetentionPolicy.RUNTIME)
                @Target([ElementType.METHOD])
                public @interface SomeAnnotation {
                    Class value()
                }

                class A {
                    @SomeAnnotation(org.codehaus.groovy.runtime.GeneratedClosure.class)
                    def method(def param) { "Test" }
                }

                class A_Delegate {
                    @Delegate(methodAnnotations = true)
                    A a = new A()
                }
            """
        }
        assert message.contains('@Delegate does not support keeping Closure annotation members.')
    }

    // GROOVY-5445
    void testDelegateToSuperProperties() {
        assertScript """
            class Foo {
                @Delegate Bar delegate = new Bar()
                def foo() {
                    bar = "bar"
                    baz = "baz"
                }
            }

            class Bar extends Baz { String bar }
            class Baz { String baz }

            def f = new Foo()
            f.foo()
            assert f.bar + f.baz == 'barbaz'
        """
    }

    // GROOVY-7243
    void testInclude() {
        assertScript '''
            class Book {
                String title
                String author

                String getTitleAndAuthor() {
                    "${title} : ${author}"
                }

                String getAuthorAndTitle() {
                    "${author} : ${title}"
                }
            }

            class OwnedBook {
                String owner

                @Delegate(includes=['author', 'getTitleAndAuthor'])
                Book book
            }
            
            Book book = new Book(title: 'Ulysses', author: 'James Joyce')
            OwnedBook ownedBook = new OwnedBook(owner: 'John Smith', book: book)

            ownedBook.author = 'John Smith'
            assert book.author == 'John Smith'

            assert ownedBook.getTitleAndAuthor() == 'Ulysses : John Smith'

            try {
                ownedBook.getAuthorAndTitle()
                assert false, 'Non-included methods should not be delegated'
            } catch(groovy.lang.MissingMethodException expected) {
            }

            try {
                ownedBook.title = 'Finnegans Wake'
                assert false, 'Non-included properties should not be delegated'
            } catch(groovy.lang.MissingPropertyException expected) {
            }
        '''
    }
    
    // GROOVY-6329
    void testIncludeAndExcludeByType() {
        assertScript """
            interface OddInclusionsTU<T, U> {
                boolean addAll(Collection<? extends T> t)
                boolean add(U u)
                T remove(int index)
            }

            interface OddInclusionsU<U> extends OddInclusionsTU<Integer, U> { }

            interface OddInclusions extends OddInclusionsU<Integer> { }

            interface OtherInclusions {
                void clear()
            }

            interface EvenExclusions extends OddInclusions, OtherInclusions { }

            class MixedNumbers {
                // collection variant of addAll and remove will work on odd list
                @Delegate(includeTypes=OddInclusions) List<Integer> odds = [1, 3]
                // clear will work on other list
                @Delegate(includeTypes=OtherInclusions) List<Integer> others = [0]
                // all other methods will work on even list
                @Delegate(excludeTypes=EvenExclusions) List<Integer> evens = [2, 4, 6]
                def getAll() { evens + odds + others }
            }

            def list = new MixedNumbers()
            assert list.all == [2, 4, 6, 1, 3, 0]
            list.add(5)
            list.addAll([7, 9])
            list.addAll(1, [8])
            list.remove(0)
            assert list.indexOf(8) == 1
            list.clear()
            assert list.all == [2, 8, 4, 6, 3, 5, 7, 9]
        """
    }

    // GROOVY-5211
    void testAvoidFieldNameClashWithParameterName() {
        assertScript """
            class A {
                def foo(a) { a * 2 }
            }

            class B {
                @Delegate A a = new A()
            }

            assert new B().foo(10) == 20
        """
    }

    // GROOVY-6542
    void testLineNumberInStackTrace() {
        try {
            assertScript '''import groovy.transform.ASTTest
    import org.codehaus.groovy.control.CompilePhase

    @ASTTest(phase=CompilePhase.CANONICALIZATION, value={
        def fieldNode = node.getDeclaredField('thingie')
        def blowupMethod = node.getDeclaredMethod('blowup')
        def mce = blowupMethod.code.expression
        assert mce.lineNumber==fieldNode.lineNumber
        assert mce.lineNumber>0
    })
    class Upper {
      @Delegate Lower thingie

      Upper() {
        thingie = new Lower()
      }
    }

    class Lower {
      def foo() {
        println("Foo!")
      }

      def blowup(String a) {
        throw new Exception("blow up with ${a}")
      }

      def blowup() {
        throw new Exception("blow up")
      }
    }

    def up = new Upper()
    up.foo()
    up.blowup("bar")
    '''
        } catch (e) {
            // ok
        }
    }

    //
    void testShouldNotReuseRawClassNode() {
        assertScript '''import org.codehaus.groovy.transform.DelegateMap
class Foo {
    DelegateMap dm = new DelegateMap()
}
def foo = new Foo()
assert foo.dm.x == '123'
'''
    }

    // GROOVY-7118
    void testDelegateOfMethodHavingPlaceholder() {
        assertScript """
            interface FooInt {
              public <T extends Throwable> T get(Class<T> clazz) throws Exception
            }

            class Foo implements FooInt {
              public <T extends Throwable> T get(Class<T> clazz) throws Exception {
                clazz.newInstance()
              }
            }

            class FooMain {
                @Delegate Foo foo = new Foo()
            }

            @groovy.transform.CompileStatic
            class FooMain2 {
                @Delegate Foo foo = new Foo()
            }

            assert new FooMain().get(Exception).class == Exception
            assert new FooMain2().get(Exception).class == Exception

            import org.codehaus.groovy.transform.Bar
            class BarMain {
                @Delegate Bar bar = new Bar()
            }
            assert new BarMain().get(Exception).class == Exception
        """
    }

    // GROOVY-7261
    void testShouldWorkWithLazyTransform() {
        assertScript '''
            class Foo {
                private @Delegate @Lazy ArrayList list = ['bar', 'baz']
                // fragile: $list is an internal implementation detail that may change
                def getInternalDelegate() { $list }
            }

            def f = new Foo()
            assert f.internalDelegate == null
            assert f.size() == 2
            assert f.internalDelegate == ['bar', 'baz']
        '''
    }

    // GROOVY-6454
    void testMethodsWithInternalNameShouldNotBeDelegatedTo() {
        assertScript '''
            class HasMethodWithInternalName {
                void $() {
                }
            }

            class DelegatesToHasMethodWithInternalName {
                @Delegate
                HasMethodWithInternalName hasMethodWithInternalName
            }

            assert !new DelegatesToHasMethodWithInternalName().respondsTo('$')
        '''
    }

    // GROOVY-6454
    void testMethodsWithInternalNameShouldBeDelegatedToIfRequested() {
        assertScript '''
            interface HasMethodWithInternalName {
                void $()
            }

            class DelegatesToHasMethodWithInternalName {
                @Delegate(allNames = true)
                HasMethodWithInternalName hasMethodWithInternalName
            }

            assert new DelegatesToHasMethodWithInternalName().respondsTo('$')
        '''
    }

    // GROOVY-6454
    void testProperitesWithInternalNameShouldBeDelegatedToIfRequested() {
        assertScript '''
            class HasPropertyWithInternalName {
                def $
            }

            class DelegatesToHasPropertyWithInternalName {
                @Delegate(allNames = true)
                HasPropertyWithInternalName hasPropertyWithInternalName
            }

            def delegates = new DelegatesToHasPropertyWithInternalName()
            assert delegates.respondsTo('get$')
            assert delegates.respondsTo('set$')
        '''
    }

    void testDelegateToGetterMethod() {
        // given:
        def delegate = { new DelegateFooImpl() }
        // when:
        def foo = new FooToMethod(delegate)
        // then:
        assert foo.foo() == delegate().foo()
    }

    // GROOVY-5752
    void testDelegationShouldAccountForPrimitiveBooleanProperties() {
        assertScript """
            class A {
                boolean a
                boolean b
                boolean isB() { b }
                boolean c
                boolean getC() { c }
            }

            class B {
                @Delegate A a = new A(a: true, b: true, c: true)
            }

            def a = new A(a: true, b: true, c: true)
            assert a.getA()
            assert a.isA()
            assert a.isB()
            assert a.getC()

            def b = new B()
            assert b.getA()
            assert b.isA()
            assert b.isB()
            assert b.getC()
        """
    }

    //GROOVY-8132
    void testOwnerPropertyPreferredToDelegateProperty() {
        assertScript '''
            class Foo {
                String pls
                @groovy.lang.Delegate
                Bar bar
            }

            class Bar { 
                String pls        
            }
            assert new Foo(pls: 'ok').pls == 'ok'
        '''
    }

    void testOwnerMethodPreferredToDelegateMethod() {
        assertScript '''
            class Foo {
                String pls() { 'foo pls' }
                @groovy.lang.Delegate
                Bar bar
            }

            class Bar {
                String pls() { 'bar pls' }
            }
            assert new Foo(bar: new Bar()).pls() == 'foo pls'
        '''
    }

    // GROOVY-8204
    void testDelegateToArray() {
        assertScript '''
            import groovy.lang.Delegate

            class BugsMe {
                @Delegate
                String[] content = ['foo', 'bar']
            }

            assert new BugsMe().content.join() == 'foobar'
            assert new BugsMe().content.length == 2
            assert new BugsMe().length == 2
        '''
    }

    // GROOVY-9289
    void testExcludesWithInvalidPropertyNameResultsInError() {
        def message = shouldFail """
            class WMap {
                String name
                @Delegate(excludes = "name")
                Map<String, String> data
             
                WMap(String name, Map<String, String> data) {
                    this.name = name
                    this.data = data
                }
            }

            new WMap('example', [name: 'weird'])
        """
        assert message.contains("Error during @Delegate processing: 'excludes' property or method 'name' does not exist.")
    }

    // GROOVY-8825
    void testDelegateToPrecompiledGroovyGeneratedMethod() {
        assertScript '''
            import org.codehaus.groovy.transform.CompiledClass8825
            class B {
                @Delegate(methodAnnotations = true)
                private final CompiledClass8825 delegate = new CompiledClass8825()
            }
            assert new B().s == '456'
        '''
    }
}

interface DelegateFoo {
    def foo()
}

class DelegateFooImpl implements DelegateFoo {
    def foo() { 'foo impl' }
}

interface DelegateBar extends DelegateFoo {
    def bar()
}

class DelegateBarImpl implements DelegateBar {
    @Delegate DelegateFoo foo;

    DelegateBarImpl(DelegateFoo f) { this.foo = f}

    def bar() { 'bar impl'}
}

class BazWithDeprecatedFoo {
    @Deprecated foo() { 'foo' }
    def bar() { 'bar' }
}

class DelegateBarWithoutDeprecated {
    @Delegate BazWithDeprecatedFoo baz
}

class DelegateBarForcingDeprecated {
    @Delegate(deprecated=true) BazWithDeprecatedFoo baz
}

class Foo4244 {
    @Delegate Bar4244 bar = new Bar4244()
}

class FooToMethod {
    private final Closure<DelegateFoo> strategy

    FooToMethod(Closure<DelegateFoo> strategy) {
        this.strategy = strategy
    }

    @Delegate
    DelegateFoo getStrategy() { strategy() }
}

class Bar4244 {
    String nonFinalBaz = "Initial value - nonFinalBaz"
    final String finalBaz = "Initial value - finalBaz"
}

interface SomeInterface4619 {
    void doSomething()
}

interface SomeOtherInterface4619 extends SomeInterface4619 {}

class SomeClass4619 {
    @Delegate
    SomeOtherInterface4619 delegate
}

interface BarInt {
    public <T extends Throwable> T get(Class<T> clazz) throws Exception
}

class Bar implements BarInt {
    public <T extends Throwable> T get(Class<T> clazz) throws Exception {
        clazz.newInstance()
    }
}

class CompiledClass8825 {
    final String s = '456'
}

// DO NOT MOVE INSIDE THE TEST SCRIPT OR IT WILL NOT TEST
// WHAT IT IS SUPPOSED TO TEST ANYMORE !
class DelegateMap {
    protected final @Delegate Map props = [x:'123']
}