/*
 * Copyright 2011 the original author or authors.
 *
 * Licensed 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.gradle.integtests.fixtures.executer;

import org.gradle.util.TextUtil;
import org.hamcrest.Matcher;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import static org.gradle.util.Matchers.isEmpty;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;

public class OutputScrapingExecutionFailure extends OutputScrapingExecutionResult implements ExecutionFailure {
    private static final Pattern FAILURE_PATTERN = Pattern.compile("(?m)FAILURE: .+$");
    private static final Pattern CAUSE_PATTERN = Pattern.compile("(?m)(^\\s*> )");
    private static final Pattern DESCRIPTION_PATTERN = Pattern.compile("(?ms)^\\* What went wrong:$(.+?)^\\* Try:$");
    private static final Pattern LOCATION_PATTERN = Pattern.compile("(?ms)^\\* Where:((.+)'.+') line: (\\d+)$");
    private static final Pattern RESOLUTION_PATTERN = Pattern.compile("(?ms)^\\* Try:$(.+?)^\\* Exception is:$");
    private static final Pattern EXCEPTION_PATTERN = Pattern.compile("(?ms)^\\* Exception is:$(.+?):(.+?)$");
    private static final Pattern EXCEPTION_CAUSE_PATTERN = Pattern.compile("(?ms)^Caused by: (.+?):(.+?)$");
    private final String description;
    private final String lineNumber;
    private final String fileName;
    private final String resolution;
    private final Exception exception;
    // with normalized line endings
    private final List<String> causes = new ArrayList<String>();

    public OutputScrapingExecutionFailure(String output, String error) {
        super(output, error);

        java.util.regex.Matcher matcher = FAILURE_PATTERN.matcher(error);
        if (matcher.find()) {
            if (matcher.find()) {
                throw new AssertionError("Found multiple failure sections in build error output.");
            }
        }

        matcher = LOCATION_PATTERN.matcher(error);
        if (matcher.find()) {
            fileName = matcher.group(1).trim();
            lineNumber = matcher.group(3);
        } else {
            fileName = "";
            lineNumber = "";
        }

        matcher = DESCRIPTION_PATTERN.matcher(error);
        if (matcher.find()) {
            String problemStr = matcher.group(1);
            Problem problem = extract(problemStr);
            description = problem.description;
            causes.addAll(problem.causes);
            while (matcher.find()) {
                problemStr = matcher.group(1);
                problem = extract(problemStr);
                causes.addAll(problem.causes);
            }
        } else {
            description = "";
        }

        matcher = RESOLUTION_PATTERN.matcher(error);
        if (!matcher.find()) {
            resolution = "";
        } else {
            resolution = matcher.group(1).trim();
        }

        matcher = EXCEPTION_PATTERN.matcher(error);
        if (!matcher.find()) {
            exception = null;
        } else {
            String exceptionClass = matcher.group(1).trim();
            String exceptionMessage = matcher.group(2).trim();
            matcher = EXCEPTION_CAUSE_PATTERN.matcher(error);
            exception = recreateException(exceptionClass, exceptionMessage, matcher);
        }
    }

    private Problem extract(String problem) {
        java.util.regex.Matcher matcher = CAUSE_PATTERN.matcher(problem);
        String description;
        List<String> causes = new ArrayList<String>();
        if (!matcher.find()) {
            description = TextUtil.normaliseLineSeparators(problem.trim());
        } else {
            description = TextUtil.normaliseLineSeparators(problem.substring(0, matcher.start()).trim());
            while (true) {
                int pos = matcher.end();
                int prefix = matcher.group(1).length();
                String prefixPattern = toPrefixPattern(prefix);
                if (matcher.find(pos)) {
                    String cause = TextUtil.normaliseLineSeparators(problem.substring(pos, matcher.start()).trim().replaceAll(prefixPattern, ""));
                    causes.add(cause);
                } else {
                    String cause = TextUtil.normaliseLineSeparators(problem.substring(pos).trim().replaceAll(prefixPattern, ""));
                    causes.add(cause);
                    break;
                }
            }
        }
        return new Problem(description, causes);
    }

    private Exception recreateException(String className, String message, java.util.regex.Matcher exceptionCauseMatcher) {
        Exception causedBy = null;
        if (exceptionCauseMatcher.find()) {
            String causedByClass = exceptionCauseMatcher.group(1).trim();
            String causedByMessage = exceptionCauseMatcher.group(2).trim();
            causedBy = recreateException(causedByClass, causedByMessage, exceptionCauseMatcher);
        }
        try {
            if (causedBy == null) {
                return (Exception) Class.forName(className).getConstructor(String.class).newInstance(message);
            } else {
                return (Exception) Class.forName(className).getConstructor(String.class, Throwable.class).newInstance(message, causedBy);
            }
        } catch (Exception e) {
            return new Exception(message);
        }
    }

    private String toPrefixPattern(int prefix) {
        StringBuilder builder = new StringBuilder("(?m)^");
        for (int i = 0; i < prefix; i++) {
            builder.append(' ');
        }
        return builder.toString();
    }

    public ExecutionFailure assertHasLineNumber(int lineNumber) {
        assertThat(this.lineNumber, equalTo(String.valueOf(lineNumber)));
        return this;
    }

    public ExecutionFailure assertHasFileName(String filename) {
        assertThat(this.fileName, equalTo(filename));
        return this;
    }

    public ExecutionFailure assertHasCause(String description) {
        assertThatCause(startsWith(description));
        return this;
    }

    public ExecutionFailure assertThatCause(Matcher<String> matcher) {
        for (String cause : causes) {
            if (matcher.matches(cause)) {
                return this;
            }
        }
        fail(String.format("No matching cause found in %s", causes));
        return this;
    }

    public ExecutionFailure assertHasResolution(String resolution) {
        assertThat(this.resolution, equalTo(resolution));
        return this;
    }

    public ExecutionFailure assertHasNoCause() {
        assertThat(causes, isEmpty());
        return this;
    }

    public ExecutionFailure assertHasDescription(String context) {
        assertThatDescription(startsWith(context));
        return this;
    }

    public ExecutionFailure assertThatDescription(Matcher<String> matcher) {
        assertThat(description, matcher);
        return this;
    }

    public ExecutionFailure assertTestsFailed() {
        new DetailedExecutionFailure(this).assertTestsFailed();
        return this;
    }

    public DependencyResolutionFailure assertResolutionFailure(String configurationPath) {
        return new DependencyResolutionFailure(this, configurationPath);
    }

    public Exception getException() {
        return exception;
    }

    private static class Problem {
        final String description;
        final List<String> causes;

        private Problem(String description, List<String> causes) {
            this.description = description;
            this.causes = causes;
        }
    }
}
