/*
 *  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.
 */
class ClassTest extends GroovyTestCase {

    void testClassDefinition() {
        assertScript '''
            // tag::class_definition[]
            class Person {                       //<1>

                String name                      //<2>
                Integer age

                def increaseAge(Integer years) { //<3>
                    this.age += years
                }
            }
            // end::class_definition[]

            // tag::class_instantiation[]
            def p = new Person()
            // end::class_instantiation[]

        '''
    }

    void testInnerClass() {
        assertScript '''
            // tag::inner_class[]
            class Outer {
                private String privateStr

                def callInnerMethod() {
                    new Inner().methodA()       //<1>
                }

                class Inner {                   //<2>
                    def methodA() {
                        println "${privateStr}." //<3>
                    }
                }
            }
            // end::inner_class[]
            def o = new Outer()
        '''
    }

    void testInnerClass2() {
        assertScript '''
            // tag::inner_class2[]
            class Outer2 {
                private String privateStr = 'some string'

                def startThread() {
                   new Thread(new Inner2()).start()
                }

                class Inner2 implements Runnable {
                    void run() {
                        println "${privateStr}."
                    }
                }
            }
            // end::inner_class2[]
            def o2 = new Outer2()
        '''
    }

    void testAnonymousInnerClass() {
        assertScript '''
            // tag::anonymous_inner_class[]
            class Outer3 {
                private String privateStr = 'some string'

                def startThread() {
                    new Thread(new Runnable() {      //<1>
                        void run() {
                            println "${privateStr}."
                        }
                    }).start()                       //<2>
                }
            }
            // end::anonymous_inner_class[]
            def o3 = new Outer3()
        '''
    }

    void testAbstractClass() {
        assertScript '''
            // tag::abstract_class[]
            abstract class Abstract {         //<1>
                String name

                abstract def abstractMethod() //<2>

                def concreteMethod() {
                    println 'concrete'
                }
            }
            // end::abstract_class[]
            def ac = new Abstract() { def abstractMethod() {} }
        '''
    }

    void testConstructorPositionalParameters() {
        assertScript '''
            // tag::constructor_positional_parameters[]
            class PersonConstructor {
                String name
                Integer age

                PersonConstructor(name, age) {          //<1>
                    this.name = name
                    this.age = age
                }
            }

            def person1 = new PersonConstructor('Marie', 1)  //<2>
            def person2 = ['Marie', 2] as PersonConstructor  //<3>
            PersonConstructor person3 = ['Marie', 3]         //<4>
            // end::constructor_positional_parameters[]

            assert person1.name == 'Marie'
            assert person2.name == 'Marie'
            assert person3.name == 'Marie'
        '''
    }

    void testConstructorNamedParameters() {
        assertScript '''
            // tag::constructor_named_parameters[]
            class PersonWOConstructor {                                  //<1>
                String name
                Integer age
            }

            def person4 = new PersonWOConstructor()                      //<2>
            def person5 = new PersonWOConstructor(name: 'Marie')         //<3>
            def person6 = new PersonWOConstructor(age: 1)                //<4>
            def person7 = new PersonWOConstructor(name: 'Marie', age: 2) //<5>
            // end::constructor_named_parameters[]

            assert person4.name == null
        '''
    }

    void testInterfaceDefinition() {
        assertScript '''
            // tag::interface_def_1[]
            interface Greeter {                                         // <1>
                void greet(String name)                                 // <2>
            }
            // end::interface_def_1[]

            // tag::class_implements[]
            class SystemGreeter implements Greeter {                    // <1>
                void greet(String name) {                               // <2>
                    println "Hello $name"
                }
            }

            def greeter = new SystemGreeter()
            assert greeter instanceof Greeter                           // <3>
            // end::class_implements[]
            greeter.greet('Laura')

            // tag::extended_interface[]
            interface ExtendedGreeter extends Greeter {                 // <1>
                void sayBye(String name)
            }
            // end::extended_interface[]

            // tag::no_structural_interface[]
            class DefaultGreeter {
                void greet(String name) { println "Hello" }
            }

            greeter = new DefaultGreeter()
            assert !(greeter instanceof Greeter)
            // end::no_structural_interface[]

            def coerced
            // tag::interface_coercion[]
            greeter = new DefaultGreeter()                              // <1>
            coerced = greeter as Greeter                                // <2>
            assert coerced instanceof Greeter                           // <3>
            // end::interface_coercion[]
        '''

        def err = shouldFail {
            assertScript '''
                // tag::protected_forbidden[]
                interface Greeter {
                    protected void greet(String name)           // <1>
                }
                // end::protected_forbidden[]
                1
            '''
        }
        assert err.contains("Method 'greet' is protected but should be public in interface 'Greeter'")

        err = shouldFail {
            assertScript '''
                // tag::private_forbidden[]
                interface Greeter {
                    private void greet(String name)
                }
                // end::private_forbidden[]
                1
            '''
        }
        assert err.contains("Method 'greet' is private but should be public in interface 'Greeter'")
    }


    void testFields() {
        assertScript '''
            // tag::field_declaration[]
            class Data {
                private int id                                  // <1>
                protected String description                    // <2>
                public static final boolean DEBUG = false       // <3>
            }
            // end::field_declaration[]
            def d = new Data()
        '''
        assertScript '''
            class IDGenerator { static int next() {0} }
            // tag::field_initialization[]
            class Data {
                private String id = IDGenerator.next() // <1>
                // ...
            }
            // end::field_initialization[]
            new Data()
        '''

        assertScript '''
            // tag::typing_fields[]
            class BadPractice {
                private mapping                         // <1>
            }
            class GoodPractice {
                private Map<String,String> mapping      // <2>
            }
            // end::typing_fields[]
            BadPractice
        '''
    }

    void testProperties() {
        assertScript '''
            // tag::properties_definition[]
            class Person {
                String name                             // <1>
                int age                                 // <2>
            }
            // end::properties_definition[]

            assert Person.declaredFields.name.containsAll (['name','age'])
            assert Person.getDeclaredMethod('getName')
            assert Person.getDeclaredMethod('getAge')
            assert Person.getDeclaredMethod('setName',String)
            assert Person.getDeclaredMethod('setAge',int)
        '''

        assertScript '''
            // tag::readonly_property[]
            class Person {
                final String name                   // <1>
                final int age                       // <2>
                Person(String name, int age) {
                    this.name = name                // <3>
                    this.age = age                  // <4>
                }
            }
            // end::readonly_property[]

            def p = new Person('Bob', 42)
            assert Person.declaredFields.name.containsAll (['name','age'])
            assert Person.getDeclaredMethod('getName')
            assert Person.getDeclaredMethod('getAge')
            try {
                assert Person.getDeclaredMethod('setName',String) == null
            } catch (NoSuchMethodException e) {

            }
            try {
                assert Person.getDeclaredMethod('setAge',int) == null
            } catch (NoSuchMethodException e) {

            }

        '''

        assertScript '''
            // tag::property_access[]
            class Person {
                String name
                void name(String name) {
                    this.name = "Wonder$name"       // <1>
                }
                String wonder() {
                    this.name                       // <2>
                }
            }
            def p = new Person()
            p.name = 'Marge'                        // <3>
            assert p.name == 'Marge'                // <4>
            p.name('Marge')                         // <5>
            assert p.wonder() == 'WonderMarge'      // <6>
            // end::property_access[]
        '''

        assertScript '''
            // tag::properties_meta[]
            class Person {
                String name
                int age
            }
            def p = new Person()
            assert p.properties.keySet().containsAll(['name','age'])
            // end::properties_meta[]

        '''

        assertScript '''
            // tag::pseudo_properties[]
            class PseudoProperties {
                // a pseudo property "name"
                void setName(String name) {}
                String getName() {}

                // a pseudo read-only property "age"
                int getAge() { 42 }

                // a pseudo write-only property "groovy"
                void setGroovy(boolean groovy) {  }
            }
            def p = new PseudoProperties()
            p.name = 'Foo'                      // <1>
            assert p.age == 42                  // <2>
            p.groovy = true                     // <3>
            // end::pseudo_properties[]
        '''
    }


    void testDefineAnnotation() {
        assertScript '''
            // tag::define_annotation[]
            @interface SomeAnnotation {}
            // end::define_annotation[]
            SomeAnnotation
        '''
    }

    void testAnnotationMembers() {
        assertScript '''
            // tag::ann_member_string[]
            @interface SomeAnnotation {
                String value()                          // <1>
            }
            // end::ann_member_string[]
            SomeAnnotation
        '''
        assertScript '''
            // tag::ann_member_string_default[]
            @interface SomeAnnotation {
                String value() default 'something'      // <2>
            }
            // end::ann_member_string_default[]
            SomeAnnotation
        '''
        assertScript '''
            // tag::ann_member_int[]
            @interface SomeAnnotation {
                int step()                              // <3>
            }
            // end::ann_member_int[]
            SomeAnnotation
        '''
       assertScript '''
            // tag::ann_member_class[]
            @interface SomeAnnotation {
                Class appliesTo()                       // <4>
            }
            // end::ann_member_class[]
            SomeAnnotation
        '''
       assertScript '''
            // tag::ann_member_annotation[]
            @interface SomeAnnotation {}
            @interface SomeAnnotations {
                SomeAnnotation[] value()                // <5>
            }
            // end::ann_member_annotation[]
            SomeAnnotation
        '''
        assertScript '''
            // tag::ann_member_enum[]
            enum DayOfWeek { mon, tue, wed, thu, fri, sat, sun }
            @interface Scheduled {
                DayOfWeek dayOfWeek()                   // <6>
            }
            // end::ann_member_enum[]
            Scheduled
        '''
    }

    void testApplyAnnotation() {
        assertScript '''
            @interface SomeAnnotation {
                int value() default 0
            }

            // tag::apply_annotation_1[]
            @SomeAnnotation                 // <1>
            void someMethod() {
                // ...
            }

            @SomeAnnotation                 // <2>
            class SomeClass {}

            @SomeAnnotation String var      // <3>

            // end::apply_annotation_1[]
            someMethod()
        '''

        assertScript '''
            // tag::annotation_value_set[]
            @interface Page {
                int statusCode()
            }

            @Page(statusCode=404)
            void notFound() {
                // ...
            }
            // end::annotation_value_set[]
        '''

        assertScript '''
            // tag::annotation_value_set_option[]
            @interface Page {
                String value()
                int statusCode() default 200
            }

            @Page(value='/home')                    // <1>
            void home() {
                // ...
            }

            @Page('/users')                         // <2>
            void userList() {
                // ...
            }

            @Page(value='error',statusCode=404)     // <3>
            void notFound() {
                // ...
            }
            // end::annotation_value_set_option[]
        '''


    }

    void testAnnotationTarget() {
        assertScript '''
            // tag::ann_target[]
            import java.lang.annotation.ElementType
            import java.lang.annotation.Target

            @Target([ElementType.METHOD, ElementType.TYPE])     // <1>
            @interface SomeAnnotation {}                        // <2>
            // end::ann_target[]
            SomeAnnotation
        '''
    }

    void testAnnotationRetention() {
        assertScript '''
            // tag::ann_retention[]
            import java.lang.annotation.Retention
            import java.lang.annotation.RetentionPolicy

            @Retention(RetentionPolicy.SOURCE)                   // <1>
            @interface SomeAnnotation {}                         // <2>
            // end::ann_retention[]
            SomeAnnotation
        '''
    }

    void testClosureAsAnnotationValue() {
        assertScript '''
            import java.lang.annotation.Retention
            import java.lang.annotation.RetentionPolicy
            import java.lang.reflect.Modifier
            // tag::closure_ann_def[]
            @Retention(RetentionPolicy.RUNTIME)
            @interface OnlyIf {
                Class value()                    // <1>
            }
            // end::closure_ann_def[]

            // tag::closure_ann_example[]
            class Tasks {
                Set result = []
                void alwaysExecuted() {
                    result << 1
                }
                @OnlyIf({ jdk>=6 })
                void supportedOnlyInJDK6() {
                    result << 'JDK 6'
                }
                @OnlyIf({ jdk>=7 && windows })
                void requiresJDK7AndWindows() {
                    result << 'JDK 7 Windows'
                }
            }
            // end::closure_ann_example[]

            // tag::closure_ann_runner[]
            class Runner {
                static <T> T run(Class<T> taskClass) {
                    def tasks = taskClass.newInstance()                                         // <1>
                    def params = [jdk:6, windows: false]                                        // <2>
                    tasks.class.declaredMethods.each { m ->                                     // <3>
                        if (Modifier.isPublic(m.modifiers) && m.parameterTypes.length == 0) {   // <4>
                            def onlyIf = m.getAnnotation(OnlyIf)                                // <5>
                            if (onlyIf) {
                                Closure cl = onlyIf.value().newInstance(tasks,tasks)            // <6>
                                cl.delegate = params                                            // <7>
                                if (cl()) {                                                     // <8>
                                    m.invoke(tasks)                                             // <9>
                                }
                            } else {
                                m.invoke(tasks)                                                 // <10>
                            }
                        }
                    }
                    tasks                                                                       // <11>
                }
            }
            // end::closure_ann_runner[]

            // tag::closure_ann_runner_exec[]
            def tasks = Runner.run(Tasks)
            assert tasks.result == [1, 'JDK 6'] as Set
            // end::closure_ann_runner_exec[]
        '''
    }

    void testAnnotationCollector() {
        assertScript '''import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

            @Retention(RetentionPolicy.RUNTIME)
            @interface Service {}
            @Retention(RetentionPolicy.RUNTIME)
            @interface Transactional {}

            // tag::transactionalservice_class[]
            @Service
            @Transactional
            class MyTransactionalService {}
            // end::transactionalservice_class[]

            MyTransactionalService
        '''

        assertScript '''import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy

            @Retention(RetentionPolicy.RUNTIME)
            @interface Service {}
            @Retention(RetentionPolicy.RUNTIME)
            @interface Transactional {}

            // tag::metaann_ts[]
            import groovy.transform.AnnotationCollector

            @Service                                        // <1>
            @Transactional                                  // <2>
            @AnnotationCollector                            // <3>
            @interface TransactionalService {
            }
            // end::metaann_ts[]

            // tag::transactionalservice_class2[]
            @TransactionalService                           // <1>
            class MyTransactionalService {}
            // end::transactionalservice_class2[]

            // tag::annotations_expanded[]
            def annotations = MyTransactionalService.annotations*.annotationType()
            assert (Service in annotations)
            assert (Transactional in annotations)
            // end::annotations_expanded[]
        '''
    }

    void testAnnotationCollectorWithParams() {
        assertScript '''
            import java.lang.annotation.Retention
            import java.lang.annotation.RetentionPolicy
            import groovy.transform.AnnotationCollector

            @Retention(RetentionPolicy.RUNTIME)
            @interface Timeout { int after() }
            @Retention(RetentionPolicy.RUNTIME)
            @interface Dangerous { String type() }

            // tag::collected_ann_explosive[]
            @Timeout(after=3600)
            @Dangerous(type='explosive')
            // end::collected_ann_explosive[]
            // tag::collector_ann_explosive[]
            @AnnotationCollector
            public @interface Explosive {}
            // end::collector_ann_explosive[]

            // tag::example_bomb[]
            @Explosive(after=0)                 // <1>
            class Bomb {}
            // end::example_bomb[]
            @Explosive
            class ReferenceBomb {}

            assert Bomb.getAnnotation(Timeout).after() == 0
            assert ReferenceBomb.getAnnotation(Timeout).after() == 3600
        '''
    }

    void testAnnotationCollectorWithSameParamNames() {
        assertScript '''

import groovy.transform.AnnotationCollector

import java.lang.annotation.Retention
            import java.lang.annotation.RetentionPolicy

            // tag::collector_ann_same_values[]
            @Retention(RetentionPolicy.RUNTIME)
            public @interface Foo {
               String value()                                   // <1>
            }
            @Retention(RetentionPolicy.RUNTIME)
            public @interface Bar {
                String value()                                  // <2>
            }

            @Foo
            @Bar
            @AnnotationCollector
            public @interface FooBar {}                         // <3>

            @Foo('a')
            @Bar('b')
            class Bob {}                                        // <4>

            assert Bob.getAnnotation(Foo).value() == 'a'        // <5>
            println Bob.getAnnotation(Bar).value() == 'b'       // <6>

            @FooBar('a')
            class Joe {}                                        // <7>
            assert Joe.getAnnotation(Foo).value() == 'a'        // <8>
            println Joe.getAnnotation(Bar).value() == 'a'       // <9>
            // end::collector_ann_same_values[]
        '''

        def err = shouldFail '''

            import groovy.transform.AnnotationCollector

            import java.lang.annotation.Retention
            import java.lang.annotation.RetentionPolicy

            @Retention(RetentionPolicy.RUNTIME)
            public @interface Foo {
               String value()
            }
            @Retention(RetentionPolicy.RUNTIME)
            public @interface Bar {
                int value()
            }

            @Foo
            @Bar
            @AnnotationCollector
            public @interface FooBar {}

            @FooBar(666)
            class Evil {}
            Evil
        '''
        assert err =~ /Attribute 'value' should have type 'java.lang.String'; but found type 'int' in @Foo/
    }

    void testCustomProcessor() {
        assertScript '''import groovy.transform.AnnotationCollector
            import groovy.transform.CompileStatic
            import groovy.transform.TypeCheckingMode

            // tag::compiledynamic_naive[]
            @CompileStatic(TypeCheckingMode.SKIP)
            @AnnotationCollector
            public @interface CompileDynamic {}
            // end::compiledynamic_naive[]

            @CompileDynamic
            class Foo {}
            Foo
        '''

        assertScript '''import groovy.transform.AnnotationCollector
            import groovy.transform.CompileStatic
            import groovy.transform.TypeCheckingMode
            import org.codehaus.groovy.ast.AnnotatedNode
            import org.codehaus.groovy.ast.AnnotationNode
            import org.codehaus.groovy.ast.ClassHelper
            import org.codehaus.groovy.ast.ClassNode
            import org.codehaus.groovy.ast.expr.ClassExpression
            import org.codehaus.groovy.ast.expr.PropertyExpression
            import org.codehaus.groovy.control.SourceUnit
            import org.codehaus.groovy.transform.AnnotationCollectorTransform

            // tag::compiledynamic_def_fixed[]
            @AnnotationCollector(processor = "org.codehaus.groovy.transform.CompileDynamicProcessor")
            public @interface CompileDynamic {
            }
            // end::compiledynamic_def_fixed[]

            // tag::compiledynamic_processor[]
            @CompileStatic                                                                  // <1>
            class CompileDynamicProcessor extends AnnotationCollectorTransform {            // <2>
                private static final ClassNode CS_NODE = ClassHelper.make(CompileStatic)    // <3>
                private static final ClassNode TC_NODE = ClassHelper.make(TypeCheckingMode) // <4>

                List<AnnotationNode> visit(AnnotationNode collector,                        // <5>
                                           AnnotationNode aliasAnnotationUsage,             // <6>
                                           AnnotatedNode aliasAnnotated,                    // <7>
                                           SourceUnit source) {                             // <8>
                    def node = new AnnotationNode(CS_NODE)                                  // <9>
                    def enumRef = new PropertyExpression(
                        new ClassExpression(TC_NODE), "SKIP")                               // <10>
                    node.addMember("value", enumRef)                                        // <11>
                    Collections.singletonList(node)                                         // <12>
                }
            }
            // end::compiledynamic_processor[]

            @CompileDynamic
            class Foo {}
            Foo
        '''
    }
}
