/*
 * Licensed to ObjectStyle LLC under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ObjectStyle LLC 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 io.bootique.junit5;

import io.bootique.BQRuntime;
import io.bootique.command.CommandOutcome;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.support.AnnotationSupport;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * Starts runtimes annotated with @{@link BQApp}. Runtimes must be static variables and are started once per test class
 * and are shut down when all tests in this class finish.
 *
 * @since 2.0
 */
public class BQTestExtension implements BeforeAllCallback, AfterAllCallback {

    // usijg JUnit logger
    private static final Logger logger = LoggerFactory.getLogger(BQTestExtension.class);

    private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(BQTestExtension.class);

    private static final String RUNNING_SHARED_RUNTIMES = "runningSharedRuntimes";

    @Override
    public void beforeAll(ExtensionContext context) {
        ExtensionContext.Store store = context.getStore(NAMESPACE);
        Class<?> testType = context.getRequiredTestClass();
        ReflectionUtils
                .findFields(testType, isRunnable(), ReflectionUtils.HierarchyTraversalMode.TOP_DOWN)
                .stream()
                .map(f -> getInstance(null, f))
                .forEach(r -> startAndRegisterForShutdown(r, store));
    }

    @Override
    public void afterAll(ExtensionContext context) {
        shutdown(context.getStore(NAMESPACE));
    }

    protected void shutdown(ExtensionContext.Store store) {
        List<TestRuntime> running = store.get(RUNNING_SHARED_RUNTIMES, List.class);
        if (running != null) {
            running.forEach(TestRuntime::shutdown);
        }
    }

    protected TestRuntime getInstance(Object testInstance, Field field) {

        field.setAccessible(true);
        BQRuntime runtime;
        try {
            runtime = (BQRuntime) field.get(testInstance);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Error reading runtime field", e);
        }

        Preconditions.notNull(runtime, () -> "Runtime instance '" + field.getName() + "' must be initialized explicitly");
        return new TestRuntime(runtime, field.getName(), field.getAnnotation(BQApp.class));
    }

    protected void startAndRegisterForShutdown(TestRuntime runtime, ExtensionContext.Store store) {

        if (!runtime.skipRun()) {
            CommandOutcome out = runtime.run();
            assertTrue(out.isSuccess(), () -> "Runtime '" + runtime.name + " failed to start: " + out);
        }

        if (runtime.immediateShutdown()) {
            // should we warn of quick shutdown of daemon apps? Or apps that were not run?
            runtime.shutdown();
        } else {
            store.getOrComputeIfAbsent(RUNNING_SHARED_RUNTIMES, s -> new ArrayList<>(), List.class).add(runtime);
        }
    }

    protected Predicate<Field> isRunnable() {
        return f -> {

            // provide diagnostics for misapplied or missing annotations
            // TODO: will it be actually more useful to throw instead of print a warning?
            if (AnnotationSupport.isAnnotated(f, BQApp.class)) {

                if (!BQRuntime.class.isAssignableFrom(f.getType())) {
                    logger.warn(() -> "Field '" + f.getName() + "' is annotated with @BQRun but is not a BQRuntime. Ignoring...");
                    return false;
                }

                if (!ReflectionUtils.isStatic(f)) {
                    logger.warn(() -> "BQRuntime field '" + f.getName() + "' is annotated with @BQRun but is not static. Ignoring...");
                    return false;
                }

                return true;
            }

            return false;
        };
    }

    static class TestRuntime {

        private BQRuntime runtime;
        private String name;
        private BQApp config;

        public TestRuntime(BQRuntime runtime, String name, BQApp config) {
            this.runtime = runtime;
            this.name = name;
            this.config = config;
        }

        public boolean skipRun() {
            return config.skipRun();
        }

        public boolean immediateShutdown() {
            return config.immediateShutdown();
        }

        public CommandOutcome run() {
            logger.debug(() -> "Starting Bootique runtime '" + name + "'...");
            return runtime.run();
        }

        public void shutdown() {
            logger.debug(() -> "Stopping Bootique runtime '" + name + "'...");
            try {
                runtime.shutdown();
            } catch (Exception e) {
                // ignore...
            }
        }
    }
}