/*
 *  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 groovy.util.logging

import ch.qos.logback.classic.Level
import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.classic.spi.LoggingEvent
import ch.qos.logback.core.OutputStreamAppender
import ch.qos.logback.core.layout.EchoLayout
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.slf4j.LoggerFactory

import java.lang.reflect.Modifier

import static groovy.test.GroovyAssert.assertScript
import static groovy.test.GroovyAssert.shouldFail

/**
 * Tests for {@link groovy.util.logging.Slf4j Slf4j} AST transformation.
 */
final class Slf4jTest {

    private static class LogbackInterceptingAppender<E> extends OutputStreamAppender<E> {

        private List<LoggingEvent> events = []

        List<LoggingEvent> getEvents() {
            return events
        }

        protected void append(E event) {
            if (event instanceof LoggingEvent) {
                events.add(event)
                super.append(event)
            } else {
                throw new RuntimeException('Unable to intercept logging events - probably API has changed')
            }
        }
    }

    private LogbackInterceptingAppender appender
    private Logger logger

    @BeforeEach
    void setUp() {
        appender = new LogbackInterceptingAppender()
        appender.outputStream = new ByteArrayOutputStream()
        appender.context = (LoggerContext) LoggerFactory.getILoggerFactory()
        appender.layout = new EchoLayout()
        appender.name = 'MyAppender'
        appender.start()

        logger = LoggerFactory.getLogger('MyClass')
        logger.addAppender(appender)
        logger.level = Level.ALL
    }

    @AfterEach
    void tearDown() {
        logger.detachAppender(appender)
    }

    @Test
    void testPrivateFinalStaticLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('log').modifiers.with { int modifiers ->
            assert Modifier.isPrivate(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testExplicitPrivateFinalStaticLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            import static groovy.transform.options.Visibility.*
            @groovy.transform.VisibilityOptions(value = PRIVATE)
            @groovy.util.logging.Slf4j
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('log').modifiers.with { int modifiers ->
            assert Modifier.isPrivate(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testPackagePrivateFinalStaticLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            import static groovy.transform.options.Visibility.*
            @groovy.transform.VisibilityOptions(value = PACKAGE_PRIVATE)
            @groovy.util.logging.Slf4j
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('log').modifiers.with { int modifiers ->
            assert !Modifier.isPrivate(modifiers)
            assert !Modifier.isProtected(modifiers)
            assert !Modifier.isPublic(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testProtectedFinalStaticLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            import static groovy.transform.options.Visibility.*
            @groovy.transform.VisibilityOptions(value = PROTECTED)
            @groovy.util.logging.Slf4j
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('log').modifiers.with { int modifiers ->
            assert Modifier.isProtected(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testPublicFinalStaticLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            import static groovy.transform.options.Visibility.*
            @groovy.transform.VisibilityOptions(value = PUBLIC)
            @groovy.util.logging.Slf4j
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('log').modifiers.with { int modifiers ->
            assert Modifier.isPublic(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testPrivateFinalStaticNamedLogFieldAppears() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j('logger')
            class MyClass {
            }
        ''')

        clazz.getDeclaredField('logger').modifiers.with { int modifiers ->
            assert Modifier.isPrivate(modifiers)
            assert Modifier.isStatic(modifiers)
            assert Modifier.isTransient(modifiers)
            assert Modifier.isFinal(modifiers)
        }
    }

    @Test
    void testClassAlreadyHasLogField() {
        shouldFail '''
            @groovy.util.logging.Slf4j
            class MyClass {
                String log
            }
        '''
    }

    @Test
    void testClassAlreadyHasNamedLogField() {
        shouldFail '''
            @groovy.util.logging.Slf4j('logger')
            class MyClass {
                String logger
            }
        '''
    }

    @Test
    void testLogInfo() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j
            class MyClass {
                def loggingMethod() {
                    log.error ('error called')
                    log.warn  ('warn called')
                    log.info  ('info called')
                    log.debug ('debug called')
                    log.trace ('trace called')
                }
            }
            new MyClass().loggingMethod()
        ''')

        Script s = (Script) clazz.newInstance()
        s.run()

        def events = appender.events
        int ind = 0
        assert events.size() == 5
        assert events[ind].level == Level.ERROR
        assert events[ind].message == 'error called'
        assert events[++ind].level == Level.WARN
        assert events[ind].message == 'warn called'
        assert events[++ind].level == Level.INFO
        assert events[ind].message == 'info called'
        assert events[++ind].level == Level.DEBUG
        assert events[ind].message == 'debug called'
        assert events[++ind].level == Level.TRACE
        assert events[ind].message == 'trace called'
    }

    @Test
    void testLogFromStaticMethods() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j
            class MyClass {
                static loggingMethod() {
                    log.info('(static) info called')
                }
            }
            MyClass.loggingMethod()
        ''')

        Script s = (Script) clazz.newInstance()
        s.run()

        def events = appender.events
        assert events.size() == 1
        assert events[0].level == Level.INFO
        assert events[0].message == '(static) info called'
    }

    @Test
    void testLogInfoWithNamedLogger() {
        Class clazz = new GroovyClassLoader().parseClass('''
          @groovy.util.logging.Slf4j('logger')
          class MyClass {
              def loggingMethod() {
                  logger.error ('error called')
                  logger.warn  ('warn called')
                  logger.info  ('info called')
                  logger.debug ('debug called')
                  logger.trace ('trace called')
              }
          }
          new MyClass().loggingMethod() ''')

        Script s = (Script) clazz.newInstance()
        s.run()

        def events = appender.getEvents()
        int ind = 0
        assert events.size() == 5
        assert events[ind].level == Level.ERROR
        assert events[ind].message == 'error called'
        assert events[++ind].level == Level.WARN
        assert events[ind].message == 'warn called'
        assert events[++ind].level == Level.INFO
        assert events[ind].message == 'info called'
        assert events[++ind].level == Level.DEBUG
        assert events[ind].message == 'debug called'
        assert events[++ind].level == Level.TRACE
        assert events[ind].message == 'trace called'
    }

    // GROOVY-6373
    @Test
    void testLogWithInnerClasses() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j('logger')
            class MyClass {
                def loggingMethod() {
                    logger.info('outer called')
                }
                static class MyInnerClass {
                    def loggingMethod() {
                        logger.info('inner called')
                    }
                }
            }
            new MyClass().loggingMethod()
            new MyClass.MyInnerClass().loggingMethod()
        ''')

        Script s = (Script) clazz.newInstance()
        s.run()

        def events = appender.events
        int ind = 0
        assert events.size() == 2
        assert events[ind].level == Level.INFO
        assert events[ind].message == 'outer called'
        assert events[++ind].level == Level.INFO
        assert events[ind].message == 'inner called'
    }

    // GROOVY-6834
    @Test
    void testLogTransformInteractionWithAnonInnerClass() {
        assertScript '''
            @groovy.util.logging.Slf4j
            class MyClass {
                static myMethod() {
                    String message = 'hello'
                    String audience = 'world'
                    String result
                    new Runnable() {
                        void run() {
                            result = "$message $audience"
                        }
                    }.run()
                    result
                }
            }
            assert MyClass.myMethod() == 'hello world'
        '''
    }

    // GROOVY-6873
    @Test
    void testLogTransformInteractionWithAnonInnerClass2() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j
            class Channel {
                private void someMethod(String folder)  {
                    final includeHidden = false
                    new Runnable() {
                        @Override
                        public void run() {
                            if (includeHidden) {
                            }
                        }
                    }
                }

                void otherMethod() {
                    def folder // "The current scope already contains a variable of the name folder"
                }
            }
        ''')
    }

    // GROOVY-7439
    @ParameterizedTest
    @ValueSource(strings=[
        'log', // Cannot find matching method Object#debug(String)
        '((org.slf4j.Logger) log)' // No such property: log for class: C
    ])
    void testLogTrait(String log) {
        assertScript """
            @groovy.transform.CompileStatic
            @groovy.util.logging.Slf4j
            trait T {
                void test() {
                    ${log}.debug('trace')
                }
            }
            class C implements T {
            }

            new C().test()
        """
    }

    @Test
    void testLogGuard() {
        Class clazz = new GroovyClassLoader().parseClass('''
           @groovy.util.logging.Slf4j
            class MyClass {
                def loggingMethod() {
                    def isSet = false
                    log.setLevel(ch.qos.logback.classic.Level.ERROR)
                    log.trace (isSet = true)
                    return isSet
                }
            }
            new MyClass().loggingMethod()
        ''')

        Script s = (Script) clazz.newInstance()
        assert s.run() == false
    }

    @Test
    void testDefaultCategory() {
        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j
            class MyClass {
                static loggingMethod() {
                  log.info('info called')
                }
            }
        ''')

        def s = clazz.newInstance()
        s.loggingMethod()

        assert appender.events.size() == 1
    }

    @Test
    void testCustomCategory() {
        LogbackInterceptingAppender appenderForCustomCategory = new LogbackInterceptingAppender()
        appenderForCustomCategory.outputStream = new ByteArrayOutputStream()
        appenderForCustomCategory.layout = new EchoLayout()
        appenderForCustomCategory.start()

        Logger loggerForCustomCategory = LoggerFactory.getLogger('customCategory')
        loggerForCustomCategory.addAppender(appenderForCustomCategory)

        Class clazz = new GroovyClassLoader().parseClass('''
            @groovy.util.logging.Slf4j(category='customCategory')
            class MyClass {
                static loggingMethod() {
                  log.error('error called')
                }
            }
        ''')
        def s = clazz.newInstance()
        s.loggingMethod()

        assert appenderForCustomCategory.events.size() == 1
        assert appender.events.isEmpty()
    }
}
