package ru.vyarus.dropwizard.guice.test.jupiter.ext;

import com.google.common.base.Preconditions;
import io.dropwizard.Application;
import io.dropwizard.Configuration;
import io.dropwizard.testing.DropwizardTestSupport;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.platform.commons.support.AnnotationSupport;
import ru.vyarus.dropwizard.guice.hook.GuiceyConfigurationHook;
import ru.vyarus.dropwizard.guice.test.TestCommand;
import ru.vyarus.dropwizard.guice.test.jupiter.TestGuiceyApp;
import ru.vyarus.dropwizard.guice.test.util.ConfigOverrideUtils;
import ru.vyarus.dropwizard.guice.test.util.HooksUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * {@link TestGuiceyApp} junit 5 extension implementation. Normally, extension should be activated with annotation,
 * but in some cases manual registration may be used:
 * <pre>{@code @RegisterExtension
 * static TestGuiceyAppExtension app = TestGuiceyAppExtension.forApp(MyApp.class).create()
 * }</pre>
 * This is complete equivalent of annotation declaration! Static modifier is important! There is no additional
 * methods in extension (intentionally), so registration type changes nothing in usage.
 * <p>
 * Reasons why it could be used instead of annotation:
 * <ul>
 *     <li>Incorrect execution order with some other extensions. Manually registered extension will execute
 *     after(!) all class level registrations (junit native behaviour). So moving guicey extension to manual
 *     registration may guarantee its execution after some other extension.</li>
 *     <li>Manual registration allows short hook declarations with lambdas:
 *     {@code .hooks(builder -> builder.modules(new DebugGuiceModule()))}</li>
 * </ul>
 * <p>
 * You can't use manual registration to configure multiple applications because junit allows only one extension
 * instance (if you really need to use multiple applications in tests then register one with extension and for
 * another use {@link DropwizardTestSupport} directly).
 * <p>
 * If both declarations will be used at the same class (don't do that!) then annotation will win and manual
 * registration will be ignored (junit default behaviour).
 * <p>
 * Other extensions requiring access to dropwizard application may use
 * {@link GuiceyExtensionsSupport#lookupSupport(ExtensionContext)}.
 *
 * @author Vyacheslav Rusakov
 * @since 29.04.2020
 */
public class TestGuiceyAppExtension extends GuiceyExtensionsSupport {

    private Config config;

    public TestGuiceyAppExtension() {
        // for usage with annotation
    }

    private TestGuiceyAppExtension(final Config config) {
        this.config = config;
    }

    /**
     * Builder for manual extension registration with {@link RegisterExtension}. Provides the same configuration
     * options as {@link TestGuiceyApp} annotation (annotation considered as preferred usage way).
     * <p>
     * IMPORTANT: extension must be used with static field only! You can't register multiple extensions!
     * <p>
     * This is just a different way of extension configuration! Resulted extension object does not provide any
     * additional methods (and not intended to be used at all)!
     * <p>
     * Pure {@link DropwizardTestSupport} provides an ability to register custom {@link io.dropwizard.lifecycle.Managed}
     * or listener {@link DropwizardTestSupport#addListener(DropwizardTestSupport.ServiceListener)}. If you need these
     * then use {@link Builder#hooks(GuiceyConfigurationHook...)} to register additional managed object or
     * additional dropwizard bundle (which will be the same as listener above).
     *
     * @param app application class
     * @return builder for extension configuration.
     */
    public static Builder forApp(final Class<? extends Application> app) {
        return new Builder(app);
    }

    @Override
    protected DropwizardTestSupport<?> prepareTestSupport(final ExtensionContext context) {
        if (config == null) {
            // Configure from annotation
            // Note that it is impossible to have both manually build config and annotation because annotation
            // will be processed first and manual registration will be simply ignored

            final TestGuiceyApp ann = AnnotationSupport
                    // also search annotation inside other annotations (meta)
                    .findAnnotation(context.getElement(), TestGuiceyApp.class).orElse(null);

            // catch incorrect usage by direct @ExtendWith(...)
            Preconditions.checkNotNull(ann, "%s annotation not declared: can't work without configuration, "
                            + "so either use annotation or extension with @%s for manual configuration",
                    TestGuiceyApp.class.getSimpleName(),
                    RegisterExtension.class.getSimpleName());
            config = Config.parse(ann);
        }

        HooksUtil.register(config.hooks);

        // config overrides work through system properties so it is important to have unique prefixes
        final String configPrefix = ConfigOverrideUtils.createPrefix(context.getRequiredTestClass());
        return create(context, config.app, config.configPath, configPrefix, config.configOverrides);
    }

    @SuppressWarnings({"unchecked", "checkstyle:Indentation"})
    private <C extends Configuration> DropwizardTestSupport<C> create(
            final ExtensionContext context,
            final Class<? extends Application> app,
            final String configPath,
            final String configPrefix,
            final String... overrides) {
        // NOTE: DropwizardTestSupport.ServiceListener listeners would be called ONLY on start!
        return new DropwizardTestSupport<>((Class<? extends Application<C>>) app,
                configPath,
                configPrefix,
                application -> {
                    final TestCommand<C> cmd = new TestCommand<>(application);
                    // need to hold command itself in order to properly shutdown it later
                    getExtensionStore(context).put(TestCommand.class, cmd);
                    return cmd;
                },
                ConfigOverrideUtils.convert(configPrefix, overrides));
    }

    @Override
    protected void onShutdown(final ExtensionContext context) {
        // DropwizardTestSupport would not be able to shutdown guicey task, so shutdown must be performed manually
        final TestCommand<?> command = (TestCommand<?>) getExtensionStore(context).get(TestCommand.class);
        if (command != null) {
            command.stop();
        }
    }

    /**
     * Builder used for manual extension registration ({@link #forApp(Class)}).
     */
    public static class Builder {
        private final Config cfg = new Config();

        public Builder(final Class<? extends Application> app) {
            this.cfg.app = Preconditions.checkNotNull(app, "Application class must be provided");
        }

        /**
         * Same as {@link TestGuiceyApp#config()}.
         *
         * @param configPath configuration file path
         * @return builder instance for chained calls
         */
        public Builder config(final String configPath) {
            cfg.configPath = configPath;
            return this;
        }

        /**
         * Same as {@link TestGuiceyApp#configOverride()}. Multiple calls will not be merged!
         *
         * @param values overriding configuration values in "key: value" format
         * @return builder instance for chained calls
         */
        public Builder configOverrides(final String... values) {
            cfg.configOverrides = values;
            return this;
        }

        /**
         * Same as {@link TestGuiceyApp#hooks()}. May be called multiple times.
         * <p>
         * Anonymous hooks could be declared with a static field:
         * {@code @EnableHook static GuiceyConfigurationHook hook = builder -> builder.disableExtension(
         * Something.class)}.
         * All such fields will be detected automatically and hooks registered. Hooks declared in base test classes
         * are also counted.
         *
         * @param hooks hook classes to use
         * @return builder instance for chained calls
         */
        public Builder hooks(final Class<? extends GuiceyConfigurationHook> hooks) {
            if (cfg.hooks == null) {
                cfg.hooks = HooksUtil.create(hooks);
            } else {
                cfg.hooks.addAll(HooksUtil.create(hooks));
            }
            return this;
        }

        /**
         * Has no annotation equivalent. May be used for quick configurations with lambda:
         * <pre>{@code
         * .hooks(builder -> builder.modules(new DebugModule()))
         * }</pre>
         * May be called multiple times.
         * <p>
         * Also, anonymous hooks could be declared with a static field:
         * {@code @EnableHook static GuiceyConfigurationHook hook = builder -> builder.disableExtension(
         * Something.class)}.
         * All such fields will be detected automatically and hooks registered. Hooks declared in base test classes
         * are also counted.
         *
         * @param hooks hook instances (may be lambdas)
         * @return builder instance for chained calls
         */
        public Builder hooks(final GuiceyConfigurationHook... hooks) {
            if (cfg.hooks == null) {
                cfg.hooks = new ArrayList<>();
            }
            Collections.addAll(cfg.hooks, hooks);
            return this;
        }

        /**
         * Creates extension.
         * <p>
         * Note that extension must be assigned to static field! Extension instance does not provide additional
         * methods so use field and parameter injections as with annotation extension declaration.
         *
         * @return extension instance
         */
        public TestGuiceyAppExtension create() {
            return new TestGuiceyAppExtension(cfg);
        }
    }

    /**
     * Unified configuration.
     */
    @SuppressWarnings({"checkstyle:VisibilityModifier", "PMD.DefaultPackage"})
    private static class Config {
        Class<? extends Application> app;
        String configPath = "";
        String[] configOverrides = new String[0];
        List<GuiceyConfigurationHook> hooks;

        /**
         * Converts annotation to unified configuration object.
         *
         * @param ann configuration annotation
         * @return configuration instance
         */
        static Config parse(final TestGuiceyApp ann) {
            final Config res = new Config();
            res.app = ann.value();
            res.configPath = ann.config();
            res.configOverrides = ann.configOverride();
            res.hooks = HooksUtil.create(ann.hooks());
            return res;
        }
    }
}