/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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 org.apache.deltaspike.testcontrol.api.junit;

import org.apache.deltaspike.cdise.api.CdiContainer;
import org.apache.deltaspike.cdise.api.CdiContainerLoader;
import org.apache.deltaspike.core.api.config.ConfigResolver;
import org.apache.deltaspike.core.api.config.PropertyLoader;
import org.apache.deltaspike.core.spi.filter.ClassFilter;
import org.apache.deltaspike.core.spi.activation.Deactivatable;
import org.apache.deltaspike.core.spi.config.ConfigSource;
import org.apache.deltaspike.core.util.ClassDeactivationUtils;
import org.apache.deltaspike.testcontrol.api.TestControl;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;

import jakarta.inject.Named;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import static java.lang.Boolean.TRUE;

@SuppressWarnings("UnusedDeclaration")
public class CdiTestSuiteRunner extends Suite
{
    /**
     * Configuration key to define a custom configuration properties file.
     * e.g.:
     * deltaspike.testcontrol.test-container.config-file=META-INF/dsTestContainerBootConfig.properties
     */
    public static final String CUSTOM_TEST_CONTAINER_CONFIG_FILE_KEY =
        "deltaspike.testcontrol.test-container.config-file";

    /**
     * Default resource location of the property file which gets used
     * for the container bootstrap.
     * This value can be overridden by using {@link #CUSTOM_TEST_CONTAINER_CONFIG_FILE_KEY}
     */
    public static final String DEFAULT_TEST_CONTAINER_CONFIG_FILE_NAME =
        "META-INF/apache-deltaspike_test-container";

    private static final boolean STOP_CONTAINER;

    private static final ThreadLocal<Boolean> IS_CDI_TEST_RUNNER_EXECUTION = new ThreadLocal<Boolean>();

    private static volatile boolean containerStarted; //TODO

    private final Class<?> testSuiteClass;

    static
    {
        STOP_CONTAINER = TestBaseConfig.ContainerIntegration.STOP_CONTAINER;
    }

    public CdiTestSuiteRunner(Class<?> klass, RunnerBuilder builder) throws InitializationError
    {
        super(klass, builder);
        this.testSuiteClass = klass;
    }

    protected CdiTestSuiteRunner(Class<?> klass, Class<?>[] suiteClasses) throws InitializationError
    {
        super(klass, suiteClasses);
        this.testSuiteClass = klass;
    }

    protected CdiTestSuiteRunner(RunnerBuilder builder, Class<?> klass, Class<?>[] suiteClasses)
        throws InitializationError
    {
        super(builder, klass, suiteClasses);
        this.testSuiteClass = klass;
    }

    protected CdiTestSuiteRunner(Class<?> klass, List<Runner> runners) throws InitializationError
    {
        super(klass, runners);
        this.testSuiteClass = klass;
    }

    @Override
    public void run(RunNotifier notifier)
    {
        if (this.testSuiteClass == null)
        {
            throw new IllegalStateException("no test-suite class found");
        }

        CdiContainer container = CdiContainerLoader.getCdiContainer();

        if (!containerStarted)
        {
            applyTestSpecificMetaData(getTestClass().getJavaClass());

            container.boot(getTestContainerConfig());
            containerStarted = true;
        }

        notifier.addListener(new LogRunListener());

        try
        {
            super.run(notifier);
        }
        finally
        {
            if (STOP_CONTAINER)
            {
                container.shutdown();
                containerStarted = false;
            }
        }
    }


    public static boolean isContainerStarted()
    {
        return containerStarted;
    }

    static Boolean isStopContainerAllowed()
    {
        return STOP_CONTAINER;
    }

    static ThreadLocal<Boolean> getCdiTestRunnerExecutionRef()
    {
        return IS_CDI_TEST_RUNNER_EXECUTION;
    }

    static void setContainerStarted(boolean containerStarted)
    {
        CdiTestSuiteRunner.containerStarted = containerStarted;
    }

    static class LogRunListener extends RunListener
    {
        private final Logger logger = Logger.getLogger(LogRunListener.class.getName());

        LogRunListener()
        {
        }

        @Override
        public void testStarted(Description description) throws Exception
        {
            if (TRUE.equals(IS_CDI_TEST_RUNNER_EXECUTION.get()))
            {
                this.logger.info("[run] " + description.getClassName() + "#" + description.getMethodName());
            }

            super.testRunStarted(description);
        }

        @Override
        public void testFinished(Description description) throws Exception
        {
            if (TRUE.equals(IS_CDI_TEST_RUNNER_EXECUTION.get()))
            {
                this.logger.info("[finished] " + description.getClassName() + "#" + description.getMethodName());
            }

            super.testFinished(description);
        }

        @Override
        public void testFailure(Failure failure) throws Exception
        {
            if (TRUE.equals(IS_CDI_TEST_RUNNER_EXECUTION.get()))
            {
                Description description = failure.getDescription();
                this.logger.log(Level.SEVERE, "[failed] " + description.getClassName() + "#" + description.getMethodName() +
                    " message: " + failure.getMessage());
            }

            super.testFailure(failure);
        }
    }

    public static Properties getTestContainerConfig()
    {
        String cdiTestRunnerConfig = ConfigResolver.getProjectStageAwarePropertyValue(
            CUSTOM_TEST_CONTAINER_CONFIG_FILE_KEY, DEFAULT_TEST_CONTAINER_CONFIG_FILE_NAME);
        return PropertyLoader.getProperties(cdiTestRunnerConfig);
    }

    //just here, because all shared methods are in this class
    static void applyTestSpecificMetaData(Class<?> currentAnnotationSource)
    {
        TestControl testControl = currentAnnotationSource.getAnnotation(TestControl.class);
        String activeAlternativeLabel = checkForLabeledAlternativeConfig(testControl);

        initTestEnvConfig(currentAnnotationSource, activeAlternativeLabel, testControl);
    }

    private static String checkForLabeledAlternativeConfig(TestControl testControl)
    {
        String activeAlternativeLabel = "";

        if (testControl != null)
        {
            Class<? extends TestControl.Label> activeTypedAlternativeLabel =
                    testControl.activeAlternativeLabel();

            if (!TestControl.Label.class.equals(activeTypedAlternativeLabel))
            {
                Named labelName = activeTypedAlternativeLabel.getAnnotation(Named.class);

                if (labelName != null)
                {
                    activeAlternativeLabel = labelName.value();
                }
                else
                {
                    String labelClassName = activeTypedAlternativeLabel.getSimpleName();
                    activeAlternativeLabel = labelClassName.substring(0, 1).toLowerCase();

                    if (labelClassName.length() > 1)
                    {
                        activeAlternativeLabel += labelClassName.substring(1);
                    }
                }
            }
        }
        return activeAlternativeLabel;
    }

    private static void initTestEnvConfig(Class<?> testClass, String activeAlternativeLabel, TestControl testControl)
    {
        if (ClassDeactivationUtils.isActivated(TestConfigSource.class))
        {
            TestConfigSource testConfigSource = null;

            for (ConfigSource configSource : ConfigResolver.getConfigSources())
            {
                if (configSource instanceof TestConfigSource)
                {
                    //if it happens: parallel test-execution can't be supported with labeled alternatives
                    testConfigSource = (TestConfigSource) configSource;
                }
            }

            if (testConfigSource == null)
            {
                testConfigSource = new TestConfigSource();
                ConfigResolver.addConfigSources(Arrays.<ConfigSource>asList(testConfigSource));
            }

            //always set it even if it is empty (it might overrule the value of the prev. test
            testConfigSource.getProperties().put("activeAlternativeLabel", activeAlternativeLabel);

            testConfigSource.getProperties().put("activeAlternativeLabelSource", testClass.getName());

            if (testControl != null)
            {
                testConfigSource.getProperties().put(TestControl.class.getName(), testClass.getName());
                testConfigSource.getProperties().put(ClassFilter.class.getName(), testControl.classFilter().getName());
            }
            else
            {
                //reset it to avoid leaks between tests
                testConfigSource.getProperties().put(TestControl.class.getName(), TestControl.class.getName());
                testConfigSource.getProperties().put(ClassFilter.class.getName(), ClassFilter.class.getName());
            }
        }
        else
        {
            //always set it even if it is empty (it might overrule the value of the prev. test
            System.setProperty("activeAlternativeLabel", activeAlternativeLabel); //will be picked up by ds-core

            System.setProperty("activeAlternativeLabelSource", testClass.getName()); //can be used for custom logic

            if (testControl != null) //can be used for custom logic
            {
                System.setProperty(TestControl.class.getName(), testClass.getName());
                System.setProperty(ClassFilter.class.getName(), testControl.classFilter().getName());
            }
            else
            {
                //reset it to avoid leaks between tests
                System.setProperty(TestControl.class.getName(), TestControl.class.getName());
                System.setProperty(ClassFilter.class.getName(), ClassFilter.class.getName());
            }
        }
    }

    //config-sources are already stored per classloader
    //keep it public to allow type-safe deactivation (if needed)
    public static class TestConfigSource implements ConfigSource, Deactivatable
    {
        private Map<String, String> testConfig = new ConcurrentHashMap<String, String>();

        @Override
        public int getOrdinal()
        {
            return Integer.MIN_VALUE;
        }

        @Override
        public Map<String, String> getProperties()
        {
            return testConfig;
        }

        @Override
        public String getPropertyValue(String key)
        {
            return testConfig.get(key);
        }

        @Override
        public String getConfigName()
        {
            return "ds-test-config";
        }

        @Override
        public boolean isScannable()
        {
            return true;
        }
    }
}
