/*
 * 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 org.apache.maven.surefire.api.testset;

import java.util.regex.Pattern;

import org.apache.maven.surefire.shared.utils.StringUtils;
import org.apache.maven.surefire.shared.utils.io.MatchPatterns;

import static java.io.File.separatorChar;
import static java.util.regex.Pattern.compile;
import static org.apache.maven.surefire.shared.utils.StringUtils.isBlank;
import static org.apache.maven.surefire.shared.utils.io.MatchPatterns.from;
import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.PATTERN_HANDLER_SUFFIX;
import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.REGEX_HANDLER_PREFIX;
import static org.apache.maven.surefire.shared.utils.io.SelectorUtils.matchPath;

/**
 * Single pattern test filter resolved from multi pattern filter -Dtest=MyTest#test,AnotherTest#otherTest.
 * @deprecated will be renamed to ResolvedTestPattern
 */
// will be renamed to ResolvedTestPattern
@Deprecated
public final class ResolvedTest {
    /**
     * Type of patterns in ResolvedTest constructor.
     */
    public enum Type {
        CLASS,
        METHOD
    }

    private static final String CLASS_FILE_EXTENSION = ".class";

    private static final String JAVA_FILE_EXTENSION = ".java";

    private static final String WILDCARD_PATH_PREFIX = "**/";

    private static final String WILDCARD_FILENAME_POSTFIX = ".*";

    private final String classPattern;

    private final String methodPattern;

    private final boolean isRegexTestClassPattern;

    private final boolean isRegexTestMethodPattern;

    private final String description;

    private final ClassMatcher classMatcher = new ClassMatcher();

    private final MethodMatcher methodMatcher = new MethodMatcher();

    /**
     * '*' means zero or more characters<br>
     * '?' means one and only one character
     * The pattern %regex[] prefix and suffix does not appear. The regex <i>pattern</i> is always
     * unwrapped by the caller.
     *
     * @param classPattern     test class file pattern
     * @param methodPattern    test method
     * @param isRegex          {@code true} if pattern is regex
     */
    public ResolvedTest(String classPattern, String methodPattern, boolean isRegex) {
        classPattern = tryBlank(classPattern);
        methodPattern = tryBlank(methodPattern);
        description = description(classPattern, methodPattern, isRegex);

        if (isRegex && classPattern != null) {
            classPattern = wrapRegex(classPattern);
        }

        if (isRegex && methodPattern != null) {
            methodPattern = wrapRegex(methodPattern);
        }

        this.classPattern = reformatClassPattern(classPattern, isRegex);
        this.methodPattern = methodPattern;
        isRegexTestClassPattern = isRegex;
        isRegexTestMethodPattern = isRegex;
        methodMatcher.sanityCheck();
    }

    /**
     * The regex {@code pattern} is always unwrapped.
     *
     * @param type class or method
     * @param pattern pattern or regex
     * @param isRegex {@code true} if pattern is regex
     */
    public ResolvedTest(Type type, String pattern, boolean isRegex) {
        pattern = tryBlank(pattern);
        final boolean isClass = type == Type.CLASS;
        description = description(isClass ? pattern : null, !isClass ? pattern : null, isRegex);
        if (isRegex && pattern != null) {
            pattern = wrapRegex(pattern);
        }
        classPattern = isClass ? reformatClassPattern(pattern, isRegex) : null;
        methodPattern = !isClass ? pattern : null;
        isRegexTestClassPattern = isRegex && isClass;
        isRegexTestMethodPattern = isRegex && !isClass;
        methodMatcher.sanityCheck();
    }

    /**
     * Test class file pattern, e.g. org&#47;**&#47;Cat*.class<br>, or null if not any
     * and {@link #hasTestClassPattern()} returns false.
     * Other examples: org&#47;animals&#47;Cat*, org&#47;animals&#47;Ca?.class, %regex[Cat.class|Dog.*]<br>
     * <br>
     * '*' means zero or more characters<br>
     * '?' means one and only one character
     *
     * @return class pattern or regex
     */
    public String getTestClassPattern() {
        return classPattern;
    }

    public boolean hasTestClassPattern() {
        return classPattern != null;
    }

    /**
     * Test method, e.g. "realTestMethod".<br>, or null if not any and {@link #hasTestMethodPattern()} returns false.
     * Other examples: test* or testSomethin? or %regex[testOne|testTwo] or %ant[testOne|testTwo]<br>
     * <br>
     * '*' means zero or more characters<br>
     * '?' means one and only one character
     *
     * @return method pattern or regex
     */
    public String getTestMethodPattern() {
        return methodPattern;
    }

    public boolean hasTestMethodPattern() {
        return methodPattern != null;
    }

    public boolean isRegexTestClassPattern() {
        return isRegexTestClassPattern;
    }

    public boolean isRegexTestMethodPattern() {
        return isRegexTestMethodPattern;
    }

    public boolean isEmpty() {
        return classPattern == null && methodPattern == null;
    }

    public boolean matchAsInclusive(String testClassFile, String methodName) {
        testClassFile = tryBlank(testClassFile);
        methodName = tryBlank(methodName);

        return isEmpty() || alwaysInclusiveQuietly(testClassFile) || match(testClassFile, methodName);
    }

    public boolean matchAsExclusive(String testClassFile, String methodName) {
        testClassFile = tryBlank(testClassFile);
        methodName = tryBlank(methodName);

        return !isEmpty() && canMatchExclusive(testClassFile, methodName) && match(testClassFile, methodName);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        ResolvedTest that = (ResolvedTest) o;

        return (classPattern == null ? that.classPattern == null : classPattern.equals(that.classPattern))
                && (methodPattern == null ? that.methodPattern == null : methodPattern.equals(that.methodPattern));
    }

    @Override
    public int hashCode() {
        int result = classPattern != null ? classPattern.hashCode() : 0;
        result = 31 * result + (methodPattern != null ? methodPattern.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return isEmpty() ? "" : description;
    }

    private static String description(String clazz, String method, boolean isRegex) {
        String description;
        if (clazz == null && method == null) {
            description = null;
        } else if (clazz == null) {
            description = "#" + method;
        } else if (method == null) {
            description = clazz;
        } else {
            description = clazz + "#" + method;
        }
        return isRegex && description != null ? wrapRegex(description) : description;
    }

    private boolean canMatchExclusive(String testClassFile, String methodName) {
        return canMatchExclusiveMethods(testClassFile, methodName)
                || canMatchExclusiveClasses(testClassFile, methodName)
                || canMatchExclusiveAll(testClassFile, methodName);
    }

    private boolean canMatchExclusiveMethods(String testClassFile, String methodName) {
        return testClassFile == null && methodName != null && classPattern == null && methodPattern != null;
    }

    private boolean canMatchExclusiveClasses(String testClassFile, String methodName) {
        return testClassFile != null && methodName == null && classPattern != null && methodPattern == null;
    }

    private boolean canMatchExclusiveAll(String testClassFile, String methodName) {
        return testClassFile != null && methodName != null && (classPattern != null || methodPattern != null);
    }

    /**
     * Prevents {@link #match(String, String)} from throwing NPE in situations when inclusive returns true.
     *
     * @param testClassFile    path to class file
     * @return {@code true} if examined class in null and class pattern exists
     */
    private boolean alwaysInclusiveQuietly(String testClassFile) {
        return testClassFile == null && classPattern != null;
    }

    private boolean match(String testClassFile, String methodName) {
        return matchClass(testClassFile) && matchMethod(methodName);
    }

    private boolean matchClass(String testClassFile) {
        return classPattern == null || classMatcher.matchTestClassFile(testClassFile);
    }

    private boolean matchMethod(String methodName) {
        return methodPattern == null || methodName == null || methodMatcher.matchMethodName(methodName);
    }

    private static String tryBlank(String s) {
        if (s == null) {
            return null;
        } else {
            String trimmed = s.trim();
            return StringUtils.isEmpty(trimmed) ? null : trimmed;
        }
    }

    private static String reformatClassPattern(String s, boolean isRegex) {
        if (s != null && !isRegex) {
            String path = convertToPath(s);
            path = fromFullyQualifiedClass(path);
            if (path != null && !path.startsWith(WILDCARD_PATH_PREFIX)) {
                path = WILDCARD_PATH_PREFIX + path;
            }
            return path;
        } else {
            return s;
        }
    }

    private static String convertToPath(String className) {
        if (isBlank(className)) {
            return null;
        } else {
            if (className.endsWith(JAVA_FILE_EXTENSION)) {
                className = className.substring(0, className.length() - JAVA_FILE_EXTENSION.length())
                        + CLASS_FILE_EXTENSION;
            }
            return className;
        }
    }

    static String wrapRegex(String unwrapped) {
        return REGEX_HANDLER_PREFIX + unwrapped + PATTERN_HANDLER_SUFFIX;
    }

    static String fromFullyQualifiedClass(String cls) {
        if (cls.endsWith(CLASS_FILE_EXTENSION)) {
            String className = cls.substring(0, cls.length() - CLASS_FILE_EXTENSION.length());
            return className.replace('.', '/') + CLASS_FILE_EXTENSION;
        } else if (!cls.contains("/")) {
            if (cls.endsWith(WILDCARD_FILENAME_POSTFIX)) {
                String clsName = cls.substring(0, cls.length() - WILDCARD_FILENAME_POSTFIX.length());
                return clsName.contains(".") ? clsName.replace('.', '/') + WILDCARD_FILENAME_POSTFIX : cls;
            } else {
                return cls.replace('.', '/');
            }
        } else {
            return cls;
        }
    }

    private final class ClassMatcher {
        private volatile MatchPatterns cache;

        boolean matchTestClassFile(String testClassFile) {
            return ResolvedTest.this.isRegexTestClassPattern()
                    ? matchClassRegexPatter(testClassFile)
                    : matchClassPatter(testClassFile);
        }

        private MatchPatterns of(String... sources) {
            if (cache == null) {
                try {
                    checkIllegalCharacters(sources);
                    cache = from(sources);
                } catch (IllegalArgumentException e) {
                    throwSanityError(e);
                }
            }
            return cache;
        }

        private boolean matchClassPatter(String testClassFile) {
            // @todo We have to use File.separator only because the MatchPatterns is using it internally - cannot
            // override.
            String classPattern = ResolvedTest.this.classPattern;
            if (separatorChar != '/') {
                testClassFile = testClassFile.replace('/', separatorChar);
                classPattern = classPattern.replace('/', separatorChar);
            }

            if (classPattern.endsWith(WILDCARD_FILENAME_POSTFIX) || classPattern.endsWith(CLASS_FILE_EXTENSION)) {
                return of(classPattern).matches(testClassFile, true);
            } else {
                String[] classPatterns = {classPattern + CLASS_FILE_EXTENSION, classPattern};
                return of(classPatterns).matches(testClassFile, true);
            }
        }

        private boolean matchClassRegexPatter(String testClassFile) {
            String realFile = separatorChar == '/' ? testClassFile : testClassFile.replace('/', separatorChar);
            return of(classPattern).matches(realFile, true);
        }
    }

    private final class MethodMatcher {
        private volatile Pattern cache;

        boolean matchMethodName(String methodName) {
            if (ResolvedTest.this.isRegexTestMethodPattern()) {
                fetchCache();
                return cache.matcher(methodName).matches();
            } else {
                return matchPath(ResolvedTest.this.methodPattern, methodName);
            }
        }

        void sanityCheck() {
            if (ResolvedTest.this.isRegexTestMethodPattern() && ResolvedTest.this.hasTestMethodPattern()) {
                try {
                    checkIllegalCharacters(ResolvedTest.this.methodPattern);
                    fetchCache();
                } catch (IllegalArgumentException e) {
                    throwSanityError(e);
                }
            }
        }

        private void fetchCache() {
            if (cache == null) {
                int from = REGEX_HANDLER_PREFIX.length();
                int to = ResolvedTest.this.methodPattern.length() - PATTERN_HANDLER_SUFFIX.length();
                String pattern = ResolvedTest.this.methodPattern.substring(from, to);
                cache = compile(pattern);
            }
        }
    }

    private static void checkIllegalCharacters(String... expressions) {
        for (String expression : expressions) {
            if (expression.contains("#")) {
                throw new IllegalArgumentException("Extra '#' in regex: " + expression);
            }
        }
    }

    private static void throwSanityError(IllegalArgumentException e) {
        throw new IllegalArgumentException(
                "%regex[] usage rule violation, valid regex rules:\n"
                        + " * <classNameRegex>#<methodNameRegex> - "
                        + "where both regex can be individually evaluated as a regex\n"
                        + " * you may use at most 1 '#' to in one regex filter. "
                        + e.getLocalizedMessage(),
                e);
    }
}
