////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2018 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package com.puppycrawl.tools.checkstyle;

import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.AMBIGUOUS_MODULE_NAME_EXCEPTION_MESSAGE;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.BASE_PACKAGE;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.CHECK_SUFFIX;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.ModuleLoadOption.TRY_IN_ALL_REGISTERED_PACKAGES;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.NULL_LOADER_MESSAGE;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.NULL_PACKAGE_MESSAGE;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.PACKAGE_SEPARATOR;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.STRING_SEPARATOR;
import static com.puppycrawl.tools.checkstyle.PackageObjectFactory.UNABLE_TO_INSTANTIATE_EXCEPTION_MESSAGE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import org.junit.Test;

import com.puppycrawl.tools.checkstyle.api.AbstractFileSetCheck;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.LocalizedMessage;
import com.puppycrawl.tools.checkstyle.checks.annotation.AnnotationLocationCheck;
import com.puppycrawl.tools.checkstyle.internal.utils.CheckUtil;

/**
 * Enter a description of class PackageObjectFactoryTest.java.
 *
 * @author Rick Giles
 */
public class PackageObjectFactoryTest {

    private final PackageObjectFactory factory = new PackageObjectFactory(
            BASE_PACKAGE, Thread.currentThread().getContextClassLoader());

    @Test
    public void testCtorNullLoaderException1() {
        try {
            final Object test = new PackageObjectFactory(new HashSet<>(), null);
            fail("Exception is expected but got " + test);
        }
        catch (IllegalArgumentException ex) {
            assertEquals("Invalid exception message", NULL_LOADER_MESSAGE, ex.getMessage());
        }
    }

    @Test
    public void testCtorNullLoaderException2() {
        try {
            final Object test = new PackageObjectFactory("test", null);
            fail("Exception is expected but got " + test);
        }
        catch (IllegalArgumentException ex) {
            assertEquals("Invalid exception message", NULL_LOADER_MESSAGE, ex.getMessage());
        }
    }

    @Test
    public void testCtorNullPackageException1() {
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            final Object test = new PackageObjectFactory(Collections.singleton(null), classLoader);
            fail("Exception is expected but got " + test);
        }
        catch (IllegalArgumentException ex) {
            assertEquals("Invalid exception message", NULL_PACKAGE_MESSAGE, ex.getMessage());
        }
    }

    @Test
    public void testCtorNullPackageException2() {
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            final Object test = new PackageObjectFactory((String) null, classLoader);
            fail("Exception is expected but got " + test);
        }
        catch (IllegalArgumentException ex) {
            assertEquals("Invalid exception message", NULL_PACKAGE_MESSAGE, ex.getMessage());
        }
    }

    @Test
    public void testCtorNullPackageException3() {
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        try {
            final Object test = new PackageObjectFactory(Collections.singleton(null), classLoader,
                    TRY_IN_ALL_REGISTERED_PACKAGES);
            fail("Exception is expected but got " + test);
        }
        catch (IllegalArgumentException ex) {
            assertEquals("Invalid exception message", NULL_PACKAGE_MESSAGE, ex.getMessage());
        }
    }

    @Test
    public void testMakeObjectFromName()
            throws CheckstyleException {
        final Checker checker =
            (Checker) factory.createModule(
                        "com.puppycrawl.tools.checkstyle.Checker");
        assertNotNull("Checker should not be null when creating module from name", checker);
    }

    @Test
    public void testMakeCheckFromName() {
        final String name = "com.puppycrawl.tools.checkstyle.checks.naming.ConstantName";
        try {
            factory.createModule(name);
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            final LocalizedMessage exceptionMessage = new LocalizedMessage(0,
                    Definitions.CHECKSTYLE_BUNDLE, UNABLE_TO_INSTANTIATE_EXCEPTION_MESSAGE,
                    new String[] {name, null}, null, factory.getClass(), null);
            assertEquals("Invalid exception message",
                    exceptionMessage.getMessage(), ex.getMessage());
        }
    }

    @Test
    public void testCreateModuleWithNonExistName() {
        testCreateModuleWithNonExistName("NonExistClassOne");
        testCreateModuleWithNonExistName("NonExistClassTwo");
    }

    private void testCreateModuleWithNonExistName(String name) {
        try {
            factory.createModule(name);
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            final String attemptedNames = BASE_PACKAGE + PACKAGE_SEPARATOR + name + STRING_SEPARATOR
                    + name + CHECK_SUFFIX + STRING_SEPARATOR
                    + BASE_PACKAGE + PACKAGE_SEPARATOR + name + CHECK_SUFFIX;
            final LocalizedMessage exceptionMessage = new LocalizedMessage(0,
                    Definitions.CHECKSTYLE_BUNDLE, UNABLE_TO_INSTANTIATE_EXCEPTION_MESSAGE,
                    new String[] {name, attemptedNames}, null, factory.getClass(), null);
            assertEquals("Invalid exception message",
                    exceptionMessage.getMessage(), ex.getMessage());
        }
    }

    @Test
    public void testCreateObjectFromMap() throws Exception {
        final String moduleName = "Foo";
        final String name = moduleName + CHECK_SUFFIX;
        final String packageName = BASE_PACKAGE + ".packageobjectfactory.bar";
        final String fullName = packageName + PACKAGE_SEPARATOR + name;
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final PackageObjectFactory objectFactory =
                new PackageObjectFactory(packageName, classLoader);
        final Object instance1 = objectFactory.createModule(name);
        assertEquals("Invalid canonical name", fullName, instance1.getClass().getCanonicalName());
        final Object instance2 = objectFactory.createModule(moduleName);
        assertEquals("Invalid canonical name", fullName, instance2.getClass().getCanonicalName());
    }

    @Test
    public void testCreateObjectFromFullModuleNamesWithAmbiguousException() {
        final String barPackage = BASE_PACKAGE + ".packageobjectfactory.bar";
        final String fooPackage = BASE_PACKAGE + ".packageobjectfactory.foo";
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final PackageObjectFactory objectFactory = new PackageObjectFactory(
                new LinkedHashSet<>(Arrays.asList(barPackage, fooPackage)), classLoader);
        final String name = "FooCheck";
        try {
            objectFactory.createModule(name);
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            final String optionalNames = barPackage + PACKAGE_SEPARATOR + name
                    + STRING_SEPARATOR + fooPackage + PACKAGE_SEPARATOR + name;
            final LocalizedMessage exceptionMessage = new LocalizedMessage(0,
                    Definitions.CHECKSTYLE_BUNDLE, AMBIGUOUS_MODULE_NAME_EXCEPTION_MESSAGE,
                    new String[] {name, optionalNames}, null, getClass(), null);
            assertEquals("Invalid exception message",
                    exceptionMessage.getMessage(), ex.getMessage());
        }
    }

    @Test
    public void testCreateObjectFromFullModuleNamesWithCantInstantiateException() {
        final String package1 = BASE_PACKAGE + ".wrong1";
        final String package2 = BASE_PACKAGE + ".wrong2";
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final PackageObjectFactory objectFactory = new PackageObjectFactory(
                new LinkedHashSet<>(Arrays.asList(package1, package2)), classLoader);
        final String name = "FooCheck";
        final String checkName = name + CHECK_SUFFIX;
        try {
            objectFactory.createModule(name);
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            final String attemptedNames = package1 + PACKAGE_SEPARATOR + name + STRING_SEPARATOR
                    + package2 + PACKAGE_SEPARATOR + name + STRING_SEPARATOR
                    + checkName + STRING_SEPARATOR
                    + package1 + PACKAGE_SEPARATOR + checkName + STRING_SEPARATOR
                    + package2 + PACKAGE_SEPARATOR + checkName;
            final LocalizedMessage exceptionMessage = new LocalizedMessage(0,
                    Definitions.CHECKSTYLE_BUNDLE, UNABLE_TO_INSTANTIATE_EXCEPTION_MESSAGE,
                    new String[] {name, attemptedNames}, null, getClass(), null);
            assertEquals("Invalid exception message",
                    exceptionMessage.getMessage(), ex.getMessage());
        }
    }

    @Test
    public void testCreateObjectFromFullModuleNamesWithExceptionByBruteForce() {
        final String package1 = BASE_PACKAGE + ".wrong1";
        final String package2 = BASE_PACKAGE + ".wrong2";
        final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        final PackageObjectFactory objectFactory = new PackageObjectFactory(
                new LinkedHashSet<>(Arrays.asList(package1, package2)), classLoader,
                TRY_IN_ALL_REGISTERED_PACKAGES);
        final String name = "FooCheck";
        final String checkName = name + CHECK_SUFFIX;
        try {
            objectFactory.createModule(name);
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            final String attemptedNames = package1 + PACKAGE_SEPARATOR + name + STRING_SEPARATOR
                    + package2 + PACKAGE_SEPARATOR + name + STRING_SEPARATOR
                    + checkName + STRING_SEPARATOR
                    + package1 + PACKAGE_SEPARATOR + checkName + STRING_SEPARATOR
                    + package2 + PACKAGE_SEPARATOR + checkName;
            final LocalizedMessage exceptionMessage = new LocalizedMessage(0,
                    Definitions.CHECKSTYLE_BUNDLE, UNABLE_TO_INSTANTIATE_EXCEPTION_MESSAGE,
                    new String[] {name, attemptedNames}, null, getClass(), null);
            assertEquals("Invalid exception message",
                    exceptionMessage.getMessage(), ex.getMessage());
        }
    }

    @Test
    public void testCreateObjectByBruteForce() throws Exception {
        final String className = "Checker";
        final Method createModuleByBruteForce = PackageObjectFactory.class.getDeclaredMethod(
                "createModuleByTryInEachPackage", String.class);
        createModuleByBruteForce.setAccessible(true);
        final Checker checker = (Checker) createModuleByBruteForce.invoke(factory, className);
        assertNotNull("Checker should not be null when creating module from name", checker);
    }

    @Test
    public void testCreateCheckByBruteForce() throws Exception {
        final String checkName = "AnnotationLocation";
        final Method createModuleByBruteForce = PackageObjectFactory.class.getDeclaredMethod(
                "createModuleByTryInEachPackage", String.class);
        final PackageObjectFactory packageObjectFactory = new PackageObjectFactory(
            new HashSet<>(Arrays.asList(BASE_PACKAGE, BASE_PACKAGE + ".checks.annotation")),
            Thread.currentThread().getContextClassLoader(), TRY_IN_ALL_REGISTERED_PACKAGES);
        createModuleByBruteForce.setAccessible(true);
        final AnnotationLocationCheck check = (AnnotationLocationCheck) createModuleByBruteForce
                .invoke(packageObjectFactory, checkName);
        assertNotNull("Check should not be null when creating module from name", check);
    }

    @Test
    public void testCreateCheckWithPartialPackageNameByBruteForce() throws Exception {
        final String checkName = "checks.annotation.AnnotationLocation";
        final PackageObjectFactory packageObjectFactory = new PackageObjectFactory(
            new HashSet<>(Collections.singletonList(BASE_PACKAGE)),
            Thread.currentThread().getContextClassLoader(), TRY_IN_ALL_REGISTERED_PACKAGES);
        final AnnotationLocationCheck check = (AnnotationLocationCheck) packageObjectFactory
                .createModule(checkName);
        assertNotNull("Check should not be null when creating module from name", check);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testGenerateThirdPartyNameToFullModuleNameWithException() throws Exception {
        final URLClassLoader classLoader = mock(URLClassLoader.class);
        when(classLoader.getURLs()).thenThrow(IOException.class);
        final Method method = factory.getClass().getDeclaredMethod(
                "generateThirdPartyNameToFullModuleName", ClassLoader.class);
        method.setAccessible(true);
        final int size = ((Map<String, String>) method.invoke(factory, classLoader)).size();
        assertEquals("Invalid map size", 0, size);
    }

    @Test
    public void testJoinPackageNamesWithClassName() throws Exception {
        final Class<PackageObjectFactory> clazz = PackageObjectFactory.class;
        final Method method =
            clazz.getDeclaredMethod("joinPackageNamesWithClassName", String.class, Set.class);
        method.setAccessible(true);
        final Set<String> packages = Collections.singleton("test");
        final String className = "SomeClass";
        final String actual =
            String.valueOf(method.invoke(PackageObjectFactory.class, className, packages));
        assertEquals("Invalid class name", "test." + className, actual);
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testNameToFullModuleNameMap() throws Exception {
        final Set<Class<?>> classes = CheckUtil.getCheckstyleModules();
        final Class<PackageObjectFactory> packageObjectFactoryClass = PackageObjectFactory.class;
        final Field field = packageObjectFactoryClass.getDeclaredField("NAME_TO_FULL_MODULE_NAME");
        field.setAccessible(true);
        final Collection<String> canonicalNames = ((Map<String, String>) field.get(null)).values();
        assertFalse("Invalid canonical name", classes.stream()
                .anyMatch(clazz -> !canonicalNames.contains(clazz.getCanonicalName())));
    }

    @Test
    public void testConstructorFailure() {
        try {
            factory.createModule(FailConstructorFileSet.class.getName());
            fail("Exception is expected");
        }
        catch (CheckstyleException ex) {
            assertEquals("Invalid exception message",
                    "Unable to instantiate com.puppycrawl.tools.checkstyle."
                    + "PackageObjectFactoryTest$FailConstructorFileSet", ex.getMessage());
            assertEquals("Invalid exception cause class",
                    "IllegalArgumentException", ex.getCause().getCause().getClass()
                    .getSimpleName());
        }
    }

    private static final class FailConstructorFileSet extends AbstractFileSetCheck {

        private FailConstructorFileSet() {
            throw new IllegalArgumentException("Test");
        }

        @Override
        protected void processFiltered(File file, FileText fileText) {
            // not used
        }

    }

}
