/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * 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 com.android.builder.profile;

import com.android.annotations.NonNull;
import com.google.wireless.android.sdk.stats.AndroidStudioStats.GradleBuildProfileSpan.ExecutionType;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Tests for the {@link ThreadRecorder} class.
 */
public class ThreadRecorderTest {

    @Before
    public void setUp() {

        // reset for each test.
        ProcessRecorderFactory.sINSTANCE = new ProcessRecorderFactory();
        ProcessRecorderFactory.sINSTANCE.setRecordWriter(
                Mockito.mock(ProcessRecorder.ExecutionRecordWriter.class));
    }

    @Test
    public void testBasicTracing() {
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception{
                        return 10;
                    }
                });

        Assert.assertNotNull(value);
        Assert.assertEquals(10, value.intValue());
    }

    @Test
    public void testBasicNoExceptionHandling() {
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception{
                        return 10;
                    }

                    @Override
                    public void handleException(@NonNull Exception e) {
                        handlerCalled.set(true);
                    }
                });

        Assert.assertNotNull(value);
        Assert.assertEquals(10, value.intValue());
        // exception handler shouldn't have been called.
        Assert.assertFalse(handlerCalled.get());
    }

    @Test
    public void testBasicExceptionHandling() {
        final Exception toBeThrown = new Exception("random");
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        throw toBeThrown;
                    }

                    @Override
                    public void handleException(@NonNull Exception e) {
                        handlerCalled.set(true);
                        Assert.assertEquals(toBeThrown, e);
                    }
                });

        Assert.assertTrue(handlerCalled.get());
        Assert.assertNull(value);
    }

    @Test
    public void testBlocks() {
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                                ":projectName", null, new Recorder.Block<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        return 10;
                                    }
                                });
                    }
                });

        Assert.assertNotNull(value);
        Assert.assertEquals(10, value.intValue());
    }

    @Test
    public void testBlocksWithInnerException() {
        final Exception toBeThrown = new Exception("random");
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                                ":projectName", null, new Recorder.Block<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        throw toBeThrown;
                                    }

                                    @Override
                                    public void handleException(@NonNull Exception e) {
                                        handlerCalled.set(true);
                                        Assert.assertEquals(toBeThrown, e);
                                    }
                                });
                    }
                });
        Assert.assertTrue(handlerCalled.get());
        Assert.assertNull(value);
    }

    @Test
    public void testBlocksWithOuterException() {
        final Exception toBeThrown = new Exception("random");
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                                ":projectName", null, new Recorder.Block<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        return 10;
                                    }
                                });
                        throw toBeThrown;
                    }

                    @Override
                    public void handleException(@NonNull Exception e) {
                        handlerCalled.set(true);
                        Assert.assertEquals(toBeThrown, e);
                    }
                });
        Assert.assertTrue(handlerCalled.get());
        Assert.assertNull(value);
    }

    @Test
    public void testBlocksWithInnerExceptionRepackaged() {
        final Exception toBeThrown = new Exception("random");
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                                ":projectName", null, new Recorder.Block<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        throw toBeThrown;
                                    }
                                });
                    }

                    @Override
                    public void handleException(@NonNull Exception e) {
                        handlerCalled.set(true);
                        Assert.assertTrue(e instanceof RuntimeException);
                        Assert.assertEquals(toBeThrown, e.getCause());
                    }
                });
        Assert.assertTrue(handlerCalled.get());
        Assert.assertNull(value);
    }

    @Test
    public void testWithMultipleInnerBlocksWithExceptionRepackaged() {
        final Exception toBeThrown = new Exception("random");
        final AtomicBoolean handlerCalled = new AtomicBoolean(false);
        // make three layers and throw an exception from the bottom layer, ensure the exception
        // is not repackaged in a RuntimeException several time as it makes its way back up
        // to the handler.
        Integer value = ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                ":projectName", null, new Recorder.Block<Integer>() {
                    @Override
                    public Integer call() throws Exception {
                        return ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                                ":projectName", null, new Recorder.Block<Integer>() {
                                    @Override
                                    public Integer call() throws Exception {
                                        return ThreadRecorder.get().record(
                                                ExecutionType.SOME_RANDOM_PROCESSING,
                                                ":projectName", null, new Recorder.Block<Integer>() {
                                                    @Override
                                                    public Integer call() throws Exception {
                                                        throw toBeThrown;
                                                    }
                                                });
                                    }
                                });
                    }

                    @Override
                    public void handleException(@NonNull Exception e) {
                        handlerCalled.set(true);
                        Assert.assertTrue(e instanceof RuntimeException);
                        Assert.assertEquals(toBeThrown, e.getCause());
                    }
                });
        Assert.assertTrue(handlerCalled.get());
        Assert.assertNull(value);
    }

    @Test
    public void testExceptionPropagation() {
        final Exception toBeThrown = new Exception("random");
        try {
            ThreadRecorder.get().record(ExecutionType.SOME_RANDOM_PROCESSING,
                    ":projectName", null, new Recorder.Block<Integer>() {
                        @Override
                        public Integer call() throws Exception {
                            throw toBeThrown;
                        }
                    });
        } catch (Exception e) {
            Assert.assertEquals(toBeThrown, e.getCause());
            return;
        }
        Assert.fail("Exception not propagated.");
    }
}
