/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.test.loggerusage;

import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.message.Message;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.MessageSupplier;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.common.SuppressLoggerChecks;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.loggerusage.ESLoggerUsageChecker.WrongLoggerUsage;
import org.hamcrest.Matchers;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import static org.hamcrest.Matchers.lessThanOrEqualTo;

public class ESLoggerUsageTests extends ESTestCase {

    public void testLoggerUsageChecks() throws IOException {
        for (Method method : getClass().getMethods()) {
            if (method.getDeclaringClass().equals(getClass())) {
                if (method.getName().startsWith("check")) {
                    logger.info("Checking logger usage for method {}", method.getName());
                    InputStream classInputStream = getClass().getResourceAsStream(getClass().getSimpleName() + ".class");
                    List<WrongLoggerUsage> errors = new ArrayList<>();
                    ESLoggerUsageChecker.check(errors::add, classInputStream,
                        m -> m.equals(method.getName()) || m.startsWith("lambda$" + method.getName()));
                    if (method.getName().startsWith("checkFail")) {
                        assertFalse("Expected " + method.getName() + " to have wrong Logger usage", errors.isEmpty());
                    } else {
                        assertTrue("Method " + method.getName() + " has unexpected Logger usage errors: " + errors, errors.isEmpty());
                    }
                } else {
                    assertTrue("only allow methods starting with test or check in this class", method.getName().startsWith("test"));
                }
            }
        }
    }

    public void testLoggerUsageCheckerCompatibilityWithLog4j2Logger() throws NoSuchMethodException {
        for (Method method : Logger.class.getMethods()) {
            if (ESLoggerUsageChecker.LOGGER_METHODS.contains(method.getName())) {
                assertThat(method.getParameterTypes().length, greaterThanOrEqualTo(1));
                int markerOffset = method.getParameterTypes()[0].equals(Marker.class) ? 1 : 0;
                int paramLength = method.getParameterTypes().length - markerOffset;
                if (method.isVarArgs()) {
                    assertEquals(2, paramLength);
                    assertEquals(String.class, method.getParameterTypes()[markerOffset]);
                    assertThat(method.getParameterTypes()[markerOffset + 1], Matchers.<Class<?>>isOneOf(Object[].class, Supplier[].class));
                } else {
                    assertThat(method.getParameterTypes()[markerOffset], Matchers.<Class<?>>isOneOf(Message.class, MessageSupplier.class,
                        CharSequence.class, Object.class, String.class, Supplier.class));

                    if (paramLength == 2) {
                        assertThat(method.getParameterTypes()[markerOffset + 1], Matchers.<Class<?>>isOneOf(Throwable.class, Object.class));
                        if (method.getParameterTypes()[markerOffset + 1].equals(Object.class)) {
                            assertEquals(String.class, method.getParameterTypes()[markerOffset]);
                        }
                    }
                    if (paramLength > 2) {
                        assertEquals(String.class, method.getParameterTypes()[markerOffset]);
                        assertThat(paramLength, lessThanOrEqualTo(11));
                        for (int i = 1; i < paramLength; i++) {
                            assertEquals(Object.class, method.getParameterTypes()[markerOffset + i]);
                        }
                    }
                }
            }
        }

        for (String methodName : ESLoggerUsageChecker.LOGGER_METHODS) {
            assertEquals(48, Stream.of(Logger.class.getMethods()).filter(m -> methodName.equals(m.getName())).count());
        }

        for (Constructor<?> constructor : ParameterizedMessage.class.getConstructors()) {
            assertThat(constructor.getParameterTypes().length, greaterThanOrEqualTo(2));
            assertEquals(String.class, constructor.getParameterTypes()[0]);
            assertThat(constructor.getParameterTypes()[1], Matchers.<Class<?>>isOneOf(String[].class, Object[].class, Object.class));

            if (constructor.getParameterTypes().length > 2) {
                assertEquals(3, constructor.getParameterTypes().length);
                if (constructor.getParameterTypes()[1].equals(Object.class)) {
                    assertEquals(Object.class, constructor.getParameterTypes()[2]);
                } else {
                    assertEquals(Throwable.class, constructor.getParameterTypes()[2]);
                }
            }
        }

        assertEquals(5, ParameterizedMessage.class.getConstructors().length);
    }

    public void checkNumberOfArguments1() {
        logger.info("Hello {}", "world");
    }

    public void checkFailNumberOfArguments1() {
        logger.info("Hello {}");
    }

    @SuppressLoggerChecks(reason = "test ignore functionality")
    public void checkIgnoreWhenAnnotationPresent() {
        logger.info("Hello {}");
    }

    public void checkNumberOfArguments2() {
        logger.info("Hello {}, {}, {}", "world", 2, "third argument");
    }

    public void checkFailNumberOfArguments2() {
        logger.info("Hello {}, {}", "world", 2, "third argument");
    }

    public void checkNumberOfArguments3() {
        logger.info("Hello {}, {}, {}, {}, {}, {}, {}", "world", 2, "third argument", 4, 5, 6, new String("last arg"));
    }

    public void checkFailNumberOfArguments3() {
        logger.info("Hello {}, {}, {}, {}, {}, {}, {}", "world", 2, "third argument", 4, 5, 6, 7, new String("last arg"));
    }

    public void checkNumberOfArgumentsParameterizedMessage1() {
        logger.info(new ParameterizedMessage("Hello {}, {}, {}", "world", 2, "third argument"));
    }

    public void checkFailNumberOfArgumentsParameterizedMessage1() {
        logger.info(new ParameterizedMessage("Hello {}, {}", "world", 2, "third argument"));
    }

    public void checkNumberOfArgumentsParameterizedMessage2() {
        logger.info(new ParameterizedMessage("Hello {}, {}", "world", 2));
    }

    public void checkFailNumberOfArgumentsParameterizedMessage2() {
        logger.info(new ParameterizedMessage("Hello {}, {}, {}", "world", 2));
    }

    public void checkNumberOfArgumentsParameterizedMessage3() {
        logger.info((Supplier<?>) () -> new ParameterizedMessage("Hello {}, {}, {}", "world", 2, "third argument"));
    }

    public void checkFailNumberOfArgumentsParameterizedMessage3() {
        logger.info((Supplier<?>) () -> new ParameterizedMessage("Hello {}, {}", "world", 2, "third argument"));
    }

    public void checkOrderOfExceptionArgument() {
        logger.info("Hello", new Exception());
    }

    public void checkOrderOfExceptionArgument1() {
        logger.info((Supplier<?>) () -> new ParameterizedMessage("Hello {}", "world"), new Exception());
    }

    public void checkFailOrderOfExceptionArgument1() {
        logger.info("Hello {}", "world", new Exception());
    }

    public void checkOrderOfExceptionArgument2() {
        logger.info((Supplier<?>) () -> new ParameterizedMessage("Hello {}, {}", "world", 42), new Exception());
    }

    public void checkFailOrderOfExceptionArgument2() {
        logger.info("Hello {}, {}", "world", 42, new Exception());
    }

    public void checkNonConstantMessageWithZeroArguments(boolean b) {
        logger.info(Boolean.toString(b), new Exception());
    }

    public void checkFailNonConstantMessageWithArguments(boolean b) {
        logger.info((Supplier<?>) () -> new ParameterizedMessage(Boolean.toString(b), 42), new Exception());
    }

    public void checkComplexUsage(boolean b) {
        String message = "Hello {}, {}";
        Object[] args = new Object[] { "world", 42 };
        if (b) {
            message = "also two args {}{}";
            args = new Object[] { "world", 43 };
        }
        logger.info(message, args);
    }

    public void checkFailComplexUsage1(boolean b) {
        String message = "Hello {}, {}";
        Object[] args = new Object[] { "world", 42 };
        if (b) {
            message = "just one arg {}";
            args = new Object[] { "world", 43 };
        }
        logger.info(message, args);
    }

    public void checkFailComplexUsage2(boolean b) {
        String message = "Hello {}, {}";
        Object[] args = new Object[] { "world", 42 };
        if (b) {
            message = "also two args {}{}";
            args = new Object[] { "world", 43, "another argument" };
        }
        logger.info(message, args);
    }

}
