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

import groovy.util.logging.Log
import org.codehaus.groovy.ast.ClassNode
import org.codehaus.groovy.ast.expr.AttributeExpression
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.SourceUnit
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.codehaus.groovy.control.customizers.SecureASTCustomizer
import org.codehaus.groovy.control.customizers.SourceAwareCustomizer
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

import static groovy.test.GroovyAssert.assertScript
import static groovy.test.GroovyAssert.shouldFail
import static org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder.withConfig
import static org.codehaus.groovy.syntax.Types.COMPARE_EQUAL
import static org.codehaus.groovy.syntax.Types.COMPARE_GREATER_THAN
import static org.codehaus.groovy.syntax.Types.COMPARE_GREATER_THAN_EQUAL
import static org.codehaus.groovy.syntax.Types.COMPARE_LESS_THAN
import static org.codehaus.groovy.syntax.Types.COMPARE_LESS_THAN_EQUAL
import static org.codehaus.groovy.syntax.Types.COMPARE_NOT_EQUAL
import static org.codehaus.groovy.syntax.Types.DIVIDE
import static org.codehaus.groovy.syntax.Types.MINUS
import static org.codehaus.groovy.syntax.Types.MINUS_MINUS
import static org.codehaus.groovy.syntax.Types.MULTIPLY
import static org.codehaus.groovy.syntax.Types.PLUS
import static org.codehaus.groovy.syntax.Types.PLUS_PLUS
import static org.codehaus.groovy.syntax.Types.POWER
import static org.codehaus.groovy.syntax.Types.REMAINDER

final class CustomizersTest {

    private CompilerConfiguration config;
    private GroovyShell shell;

    @BeforeEach
    void setUp() {
        config = new CompilerConfiguration()
        shell = new GroovyShell(config)
    }

    protected void assertScript(final String script) throws Exception {
        shell.evaluate(script, 'TestScript.groovy')
    }

    @Test
    void testImportCustomizer() {

        // tag::import_cz[]
        def icz = new ImportCustomizer()
        // "normal" import
        icz.addImports('java.util.concurrent.atomic.AtomicInteger', 'java.util.concurrent.ConcurrentHashMap')
        // "aliases" import
        icz.addImport('CHM', 'java.util.concurrent.ConcurrentHashMap')
        // "static" import
        icz.addStaticImport('java.lang.Math', 'PI') // import static java.lang.Math.PI
        // "aliased static" import
        icz.addStaticImport('pi', 'java.lang.Math', 'PI') // import static java.lang.Math.PI as pi
        // "star" import
        icz.addStarImports 'java.util.concurrent' // import java.util.concurrent.*
        // "static star" import
        icz.addStaticStars 'java.lang.Math' // import static java.lang.Math.*
        // end::import_cz[]

        config.addCompilationCustomizers(icz)

        assertScript '''
            def a = new AtomicInteger(123)
            def map = new CHM([:])
            assert PI == Math.PI
            assert pi == PI
            def c = {} as Callable
            assert cos(0) == 1
        '''
    }

    @Test
    void testLogCustomizer() {
        // tag::ast_cz_simple[]
        def acz = new ASTTransformationCustomizer(Log)
        config.addCompilationCustomizers(acz)
        // end::ast_cz_simple[]

        assertScript '''
            log.info "It works!"
        '''
    }

    @Test
    void testLogCustomizerWithCustomName() {
        // tag::ast_cz_customname[]
        def acz = new ASTTransformationCustomizer(Log, value: 'LOGGER')
        // use name 'LOGGER' instead of the default 'log'
        config.addCompilationCustomizers(acz)
        // end::ast_cz_customname[]

        assertScript '''
            LOGGER.info "It works!"
        '''
    }

    @Test
    void testSecureASTCustomizer() {
        // tag::secure_cz[]
        def scz = new SecureASTCustomizer()
        scz.with {
            closuresAllowed = false // user will not be able to write closures
            methodDefinitionAllowed = false // user will not be able to define methods
            allowedImports = [] // empty allowed list means imports are disallowed
            allowedStaticImports = [] // same for static imports
            allowedStaticStarImports = ['java.lang.Math'] // only java.lang.Math is allowed
            // the list of tokens the user can find
            // constants are defined in org.codehaus.groovy.syntax.Types
            allowedTokens = [ // <1>
                    PLUS,
                    MINUS,
                    MULTIPLY,
                    DIVIDE,
                    REMAINDER,
                    POWER,
                    PLUS_PLUS,
                    MINUS_MINUS,
                    COMPARE_EQUAL,
                    COMPARE_NOT_EQUAL,
                    COMPARE_LESS_THAN,
                    COMPARE_LESS_THAN_EQUAL,
                    COMPARE_GREATER_THAN,
                    COMPARE_GREATER_THAN_EQUAL,
            ].asImmutable()
            // limit the types of constants that a user can define to number types only
            allowedConstantTypesClasses = [ // <2>
                    Integer,
                    Float,
                    Long,
                    Double,
                    BigDecimal,
                    Integer.TYPE,
                    Long.TYPE,
                    Float.TYPE,
                    Double.TYPE
            ].asImmutable()
            // method calls are only allowed if the receiver is of one of those types
            // be careful, it's not a runtime type!
            allowedReceiversClasses = [ // <2>
                    Math,
                    Integer,
                    Float,
                    Double,
                    Long,
                    BigDecimal
            ].asImmutable()
        }
        // end::secure_cz[]
        config.addCompilationCustomizers(scz)
        assertScript '''
            1+1
        '''
        shouldFail {
            assertScript '''
                println "not allowed"
            '''
        }
    }

    @Test
    void testSecureASTCustomizerWithCustomChecker() {
        // tag::secure_cz_custom[]
        def scz = new SecureASTCustomizer()
        def checker = { expr ->
            !(expr instanceof AttributeExpression)
        } as SecureASTCustomizer.ExpressionChecker
        scz.addExpressionCheckers(checker)
        // end::secure_cz_custom[]
        config.addCompilationCustomizers(scz)
        shouldFail {
            assertScript '''// tag::secure_cz_custom_assert[]
class A {
    int val
}

def a = new A(val: 123)
a.@val // <1>
// end::secure_cz_custom_assert[]
'''
        }
    }

    @Test
    void testSourceAwareCustomizer() {
        // tag::source_cz[]
        def delegate = new ImportCustomizer()
        def sac = new SourceAwareCustomizer(delegate)
        // end::source_cz[]

        // tag::source_cz_predicates[]
        // the customizer will only be applied to classes contained in a file name ending with 'Bean'
        sac.baseNameValidator = { baseName ->
            baseName.endsWith 'Bean'
        }

        // the customizer will only be applied to files which extension is '.spec'
        sac.extensionValidator = { ext -> ext == 'spec' }

        // source unit validation
        // allow compilation only if the file contains at most 1 class
        sac.sourceUnitValidator = { SourceUnit sourceUnit -> sourceUnit.AST.classes.size() == 1 }

        // class validation
        // the customizer will only be applied to classes ending with 'Bean'
        sac.classValidator = { ClassNode cn -> cn.endsWith('Bean') }

        // end::source_cz_predicates[]

        config.addCompilationCustomizers(sac)
    }

    @Test
    void testCustomizerBuilder() {
        // tag::customizer_withconfig[]
        def conf = new CompilerConfiguration()
        withConfig(conf) {
            // ... <2>
        }
        // end::customizer_withconfig[]
    }
}
