/*
 * Copyright 2014 NAVER Corp.
 * 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
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * 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.navercorp.pinpoint.test.plugin;

import org.junit.runner.Description;
import org.junit.runner.manipulation.Filter;
import org.junit.runner.manipulation.NoTestsRemainException;
import org.junit.runner.manipulation.Sorter;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerScheduler;
import org.junit.runners.model.Statement;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

/**
 *
 * We have referred OrderedThreadPoolExecutor ParentRunner of JUnit.
 *
 * @author Jongho Moon
 * @author Taejin Koo
 *
 */
public class PinpointPluginTestRunner extends BlockJUnit4ClassRunner {
    private final PluginTestContext context;
    private final PinpointPluginTestInstance testCase;

    private final Object childrenLock = new Object();
    private volatile Collection<FrameworkMethod> filteredChildren = null;

    private volatile RunnerScheduler scheduler = new RunnerScheduler() {
        public void schedule(Runnable childStatement) {
            childStatement.run();
        }

        public void finished() {
            // do nothing
        }
    };

    PinpointPluginTestRunner(PluginTestContext context, PinpointPluginTestInstance testCase) throws InitializationError {
        super(context.getTestClass());

        this.context = context;
        this.testCase = testCase;
    }

    @Override
    protected String getName() {
        return String.format("[%s]", testCase.getTestId());
    }

    @Override
    protected String testName(final FrameworkMethod method) {
        return String.format("%s[%s]", method.getName(), testCase.getTestId());
    }

    @Override
    protected Statement classBlock(RunNotifier notifier) {
        return new PinpointPluginTestStatement(this, notifier, context, testCase);
    }

    @Override
    public void filter(Filter filter) throws NoTestsRemainException {
        synchronized (childrenLock) {
            List<FrameworkMethod> children = new ArrayList<FrameworkMethod>(getFilteredChildren());
            for (Iterator<FrameworkMethod> iter = children.iterator(); iter.hasNext(); ) {
                FrameworkMethod each = iter.next();
                if (shouldRun(filter, each)) {
                    try {
                        filter.apply(each);
                    } catch (NoTestsRemainException e) {
                        iter.remove();
                    }
                } else {
                    iter.remove();
                }
            }
            filteredChildren = Collections.unmodifiableCollection(children);
            if (filteredChildren.isEmpty()) {
                throw new NoTestsRemainException();
            }
        }
    }

    private Collection<FrameworkMethod> getFilteredChildren() {
        if (filteredChildren == null) {
            synchronized (childrenLock) {
                if (filteredChildren == null) {
                    filteredChildren = Collections.unmodifiableCollection(getChildren());
                }
            }
        }
        return filteredChildren;
    }

    public void sort(Sorter sorter) {
        synchronized (childrenLock) {
            for (FrameworkMethod each : getFilteredChildren()) {
                sorter.apply(each);
            }
            List<FrameworkMethod> sortedChildren = new ArrayList<FrameworkMethod>(getFilteredChildren());
            Collections.sort(sortedChildren, comparator(sorter));
            filteredChildren = Collections.unmodifiableCollection(sortedChildren);
        }
    }

    private Comparator<? super FrameworkMethod> comparator(final Sorter sorter) {
        return new Comparator<FrameworkMethod>() {
            public int compare(FrameworkMethod o1, FrameworkMethod o2) {
                return sorter.compare(describeChild(o1), describeChild(o2));
            }
        };
    }

    private void runChildren(final RunNotifier notifier) {
        final RunnerScheduler currentScheduler = scheduler;
        try {
            for (final FrameworkMethod each : getFilteredChildren()) {
                currentScheduler.schedule(new Runnable() {
                    public void run() {
                        runChild(each, notifier);
                    }
                });
            }
        } finally {
            currentScheduler.finished();
        }
    }

    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }

    boolean isAvailable(Filter filter) {
        synchronized (childrenLock) {
            List<FrameworkMethod> children = new ArrayList<FrameworkMethod>(getFilteredChildren());
            for (FrameworkMethod method : children) {
                if (shouldRun(filter, method)) {
                    return true;
                }
            }
        }

        return false;
    }

    private boolean shouldRun(Filter filter, FrameworkMethod each) {
        if (filter.shouldRun(describeChild(each))) {
            return true;
        }

        String testDescribe = PinpointPluginTestUtils.getTestDescribe(each.getMethod());
        return testDescribe.equals(filter.describe());
    }

    @Override
    public Description getDescription() {
        Description description = Description.createSuiteDescription(getName(), getRunnerAnnotations());
        for (FrameworkMethod child : getFilteredChildren()) {
            description.addChild(describeChild(child));
        }
        return description;
    }

}
