/*
 * Copyright 2010 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.initialization;

import org.gradle.api.GradleScriptException;
import org.gradle.api.internal.Contextual;
import org.gradle.api.internal.LocationAwareException;
import org.gradle.api.internal.MultiCauseException;
import org.gradle.api.tasks.TaskExecutionException;
import org.gradle.groovy.scripts.Script;
import org.gradle.groovy.scripts.ScriptSource;
import org.gradle.listener.ListenerManager;
import org.gradle.listener.ListenerNotificationException;
import org.gradle.util.JUnit4GroovyMockery;
import org.jmock.Expectations;
import org.jmock.integration.junit4.JMock;
import org.jmock.integration.junit4.JUnit4Mockery;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.util.Arrays;
import java.util.List;

import static org.gradle.util.Matchers.isEmpty;
import static org.gradle.util.WrapUtil.toArray;
import static org.gradle.util.WrapUtil.toList;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThat;

@RunWith(JMock.class)
public class DefaultExceptionAnalyserTest {
    private final JUnit4Mockery context = new JUnit4GroovyMockery();
    private final ListenerManager listenerManager = context.mock(ListenerManager.class);
    private final StackTraceElement element = new StackTraceElement("class", "method", "filename", 7);
    private final StackTraceElement callerElement = new StackTraceElement("class", "method", "filename", 11);
    private final StackTraceElement otherElement = new StackTraceElement("class", "method", "otherfile", 11);
    private final ScriptSource source = context.mock(ScriptSource.class);

    @Before
    public void setUp() {
        context.checking(new Expectations() {{
            allowing(source).getFileName();
            will(returnValue("filename"));
        }});
    }

    @Test
    public void usesOriginalExceptionWhenItIsNotAContextualException() {
        Throwable failure = new RuntimeException();

        DefaultExceptionAnalyser analyser = analyser();
        assertThat(analyser.transform(failure), sameInstance(failure));
    }

    @Test
    public void wrapsContextualExceptionWithLocationAwareException() {
        Throwable failure = new ContextualException();

        DefaultExceptionAnalyser analyser = analyser();

        Throwable transformedFailure = analyser.transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(failure));
        assertThat(gse.getCause(), sameInstance(failure));
        assertThat(gse.getReportableCauses(), isEmpty());
    }

    @Test
    public void wrapsHighestContextualExceptionWithLocationAwareException() {
        Throwable cause = new ContextualException();
        Throwable failure = new ContextualException(cause);

        DefaultExceptionAnalyser analyser = analyser();

        Throwable transformedFailure = analyser.transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(failure));
        assertThat(gse.getCause(), sameInstance(failure));
        assertThat(gse.getReportableCauses(), equalTo(toList(cause)));
    }

    @Test
    public void addsLocationInfoFromDeepestStackFrame() {
        Throwable failure = new ContextualException();
        failure.setStackTrace(toArray(element, otherElement, callerElement));

        DefaultExceptionAnalyser analyser = analyser();
        notifyAnalyser(analyser, source);

        Throwable transformedFailure = analyser.transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getScriptSource(), sameInstance(source));
        assertThat(gse.getLineNumber(), equalTo(7));
    }

    @Test
    public void addsLocationInfoFromDeepestCause() {
        RuntimeException cause = new RuntimeException();
        ContextualException failure = new ContextualException(new RuntimeException(cause));
        failure.setStackTrace(toArray(otherElement, callerElement));
        cause.setStackTrace(toArray(element, otherElement, callerElement));

        DefaultExceptionAnalyser analyser = analyser();
        notifyAnalyser(analyser, source);

        Throwable transformedFailure = analyser.transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getScriptSource(), sameInstance(source));
        assertThat(gse.getLineNumber(), equalTo(7));
    }

    @Test
    public void doesNotAddLocationWhenLocationCannotBeDetermined() {
        Throwable failure = new ContextualException();
        Throwable transformedFailure = analyser().transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getScriptSource(), nullValue());
        assertThat(gse.getLineNumber(), nullValue());
    }

    @Test
    public void wrapsContextualMultiCauseExceptionWithLocationAwareException() {
        Throwable cause1 = new ContextualException();
        Throwable cause2 = new ContextualException();
        Throwable failure = new ContextualMultiCauseException(cause1, cause2);

        Throwable transformedFailure = analyser().transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(failure));
        assertThat(gse.getCause(), sameInstance(failure));
        assertThat(gse.getReportableCauses(), equalTo(toList(cause1, cause2)));
    }

    @Test
    public void unpacksListenerNotificationException() {
        Throwable cause = new RuntimeException();
        Throwable failure = new ListenerNotificationException("broken", cause);

        Throwable transformedFailure = analyser().transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(cause));
        assertThat(gse.getCause(), sameInstance(failure));
        assertThat(gse.getReportableCauses(), isEmpty());
    }

    @Test
    public void usesOriginalExceptionWhenItIsAlreadyLocationAware() {
        Throwable failure = locationAwareException(null);

        DefaultExceptionAnalyser analyser = analyser();
        notifyAnalyser(analyser, source);
        
        assertThat(analyser.transform(failure), sameInstance(failure));
    }

    @Test
    public void usesDeepestScriptExceptionException() {
        Throwable cause = new GradleScriptException("broken", new RuntimeException());
        Throwable failure = new GradleScriptException("broken", new RuntimeException(cause));

        Throwable transformedFailure = analyser().transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(cause));
        assertThat(gse.getCause(), sameInstance(cause));
    }

    @Test
    public void usesDeepestLocationAwareException() {
        Throwable cause = locationAwareException(null);
        Throwable failure = locationAwareException(new RuntimeException(cause));

        DefaultExceptionAnalyser analyser = analyser();

        assertThat(analyser.transform(failure), sameInstance(cause));
    }

    @Test
    public void prefersScriptExceptionOverContextualException() {
        Throwable cause = new GradleScriptException("broken", new ContextualException());
        Throwable failure = new TaskExecutionException(null, cause);

        Throwable transformedFailure = analyser().transform(failure);
        assertThat(transformedFailure, instanceOf(LocationAwareException.class));

        LocationAwareException gse = (LocationAwareException) transformedFailure;
        assertThat(gse.getTarget(), sameInstance(cause));
        assertThat(gse.getCause(), sameInstance(cause));
    }

    @Test
    public void prefersLocationAwareExceptionOverScriptException() {
        Throwable cause = locationAwareException(new GradleScriptException("broken", new RuntimeException()));
        Throwable failure = new TaskExecutionException(null, cause);

        DefaultExceptionAnalyser analyser = analyser();

        assertThat(analyser.transform(failure), sameInstance(cause));
    }

    private Throwable locationAwareException(final Throwable cause) {
        final Throwable failure = context.mock(TestException.class);
        context.checking(new Expectations() {{
            allowing(failure).getCause();
            will(returnValue(cause));
            allowing(failure).getStackTrace();
            will(returnValue(toArray(element)));
        }});
        return failure;
    }

    private void notifyAnalyser(DefaultExceptionAnalyser analyser, final ScriptSource source) {
        final Script script = context.mock(Script.class);
        context.checking(new Expectations() {{
            allowing(script).getScriptSource();
            will(returnValue(source));
        }});
        analyser.beforeScript(script);
    }

    private DefaultExceptionAnalyser analyser() {
        context.checking(new Expectations() {{
            one(listenerManager).addListener(with(notNullValue(DefaultExceptionAnalyser.class)));
        }});
        return new DefaultExceptionAnalyser(listenerManager);
    }

    @Contextual
    public static class ContextualException extends RuntimeException {
        public ContextualException() {
            super("failed");
        }

        public ContextualException(Throwable throwable) {
            super(throwable);
        }
    }

    @Contextual
    public static class ContextualMultiCauseException extends RuntimeException implements MultiCauseException {
        private List<Throwable> causes;

        public ContextualMultiCauseException(Throwable... throwables) {
            this.causes = Arrays.asList(throwables);
        }

        public List<? extends Throwable> getCauses() {
            return causes;
        }

        public void initCauses(Iterable<? extends Throwable> causes) {
            this.causes.clear();
            for (Throwable cause : causes) {
                this.causes.add(cause);
            }
        }
    }

    @Contextual
    public abstract static class TestException extends LocationAwareException {
        protected TestException(Throwable cause, ScriptSource source, Integer lineNumber) {
            super(cause, cause, source, lineNumber);
        }
    }
}
