/*-
 * #%L
 * Elastic APM Java agent
 * %%
 * Copyright (C) 2018 - 2020 Elastic and contributors
 * %%
 * Licensed to Elasticsearch B.V. under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch B.V. 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.
 * #L%
 */
package co.elastic.apm.agent.configuration;

import co.elastic.apm.agent.MockTracer;
import co.elastic.apm.agent.impl.ElasticApmTracer;
import co.elastic.apm.agent.impl.GlobalTracer;
import co.elastic.apm.agent.impl.Tracer;
import co.elastic.apm.agent.sdk.ElasticApmInstrumentation;
import co.elastic.apm.agent.util.DependencyInjectingServiceLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.stagemonitor.configuration.ConfigurationOption;
import org.stagemonitor.configuration.ConfigurationOptionProvider;
import org.stagemonitor.configuration.ConfigurationRegistry;

import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * This is not an actual test.
 * This class auto-generates the config docs based on the {@link ConfigurationOptionProvider}s
 * <p>
 * The reason why this is a test class is that its a simple way to re-generate the the configuration documentation frequently -
 * every time you execute the tests.
 * </p>
 * <p>
 * Also, releases would fail if the documentation is not up-to-date as the maven release plugin executes the tests before the release
 * and checks if there are uncommitted changes.
 * </p>
 * <p>
 * As the apm-agent-java module bundles all plugins/instrumentations,
 * it has access to all configuration options of all plugins,
 * which makes this module a natural fit for generating the docs.
 * </p>
 * <p>
 * Alternatives: there is also the fmpp-maven-plugin which can render freemarker templates during the build.
 * The problem is that the model (the actual {@link ConfigurationRegistry},
 * which includes all plugin configuration classes,
 * can't be stored in a separate model file,
 * but is created dynamically in {@link #setUp()}.
 * This picks up all {@link ConfigurationOptionProvider} classes in all plugins.
 * </p>
 */
class ConfigurationExporterTest {

    private ConfigurationRegistry configurationRegistry;
    private Path renderedDocumentationPath;

    @BeforeEach
    void setUp() {
        renderedDocumentationPath = Paths.get("../docs/configuration.asciidoc");
        ElasticApmTracer tracer = mock(ElasticApmTracer.class);
        when(tracer.getState()).thenReturn(Tracer.TracerState.UNINITIALIZED);
        GlobalTracer.init(tracer);
        configurationRegistry = ConfigurationRegistry.builder()
            .optionProviders(ServiceLoader.load(ConfigurationOptionProvider.class))
            .build();
    }

    @AfterEach
    void tearDown() {
        GlobalTracer.setNoop();
    }

    @Test
    void testGeneratedConfigurationDocsAreUpToDate() throws IOException, TemplateException {
        String renderedDocumentation = renderDocumentation(configurationRegistry);
        String expected = new String(Files.readAllBytes(this.renderedDocumentationPath), StandardCharsets.UTF_8);

        // even if this test fails, we want to update the documentation
        Files.write(renderedDocumentationPath, renderedDocumentation.getBytes(StandardCharsets.UTF_8));

        assertThat(renderedDocumentation)
            .withFailMessage("The rendered configuration documentation (/docs/configuration.asciidoc) is not up-to-date.\n" +
                "If you see this error on CI, it means you have to execute the tests locally " +
                "(./mvnw clean test -pl elastic-apm-agent -am -DfailIfNoTests=false -Dtest=ConfigurationExporterTest) " +
                "which will update the rendered docs.\n" +
                "If you see this error while running the tests locally, there's nothing more to do - the rendered docs have been updated. " +
                "When you execute this test the next time, it will not fail anymore.")
            .isEqualTo(expected);
    }

    static String renderDocumentation(ConfigurationRegistry configurationRegistry) throws IOException, TemplateException {
        Configuration cfg = new Configuration(Configuration.VERSION_2_3_27);
        cfg.setClassLoaderForTemplateLoading(ConfigurationExporterTest.class.getClassLoader(), "/");
        cfg.setDefaultEncoding("UTF-8");
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);

        Template temp = cfg.getTemplate("configuration.asciidoc.ftl");
        StringWriter tempRenderedFile = new StringWriter();
        tempRenderedFile.write("////\n" +
            "This file is auto generated\n" +
            "\n" +
            "Please only make changes in configuration.asciidoc.ftl\n" +
            "////\n");
        final List<ConfigurationOption<?>> nonInternalOptions = configurationRegistry.getConfigurationOptionsByCategory()
            .values()
            .stream()
            .flatMap(List::stream)
            .filter(option -> !option.getTags().contains("internal"))
            .collect(Collectors.toList());
        final Map<String, List<ConfigurationOption<?>>> optionsByCategory = nonInternalOptions.stream()
            .collect(Collectors.groupingBy(ConfigurationOption::getConfigurationCategory, TreeMap::new, Collectors.toList()));
        temp.process(Map.of(
            "config", optionsByCategory,
            "keys", nonInternalOptions.stream().map(ConfigurationOption::getKey).sorted().collect(Collectors.toList())
        ), tempRenderedFile);

        // re-process the rendered template to resolve the ${allInstrumentationGroupNames} placeholder
        StringWriter out = new StringWriter();
        new Template("", tempRenderedFile.toString(), cfg)
            .process(Map.of("allInstrumentationGroupNames", getAllInstrumentationGroupNames()), out);

        return out.toString();
    }

    public static String getAllInstrumentationGroupNames() {
        Set<String> instrumentationGroupNames = new TreeSet<>();
        instrumentationGroupNames.add("experimental");
        for (ElasticApmInstrumentation instrumentation : DependencyInjectingServiceLoader.load(ElasticApmInstrumentation.class, MockTracer.create())) {
            instrumentationGroupNames.addAll(instrumentation.getInstrumentationGroupNames());
        }

        StringBuilder allGroups = new StringBuilder();
        for (Iterator<String> iterator = instrumentationGroupNames.iterator(); iterator.hasNext(); ) {
            allGroups.append('`').append(iterator.next()).append('`');
            if (iterator.hasNext()) {
                allGroups.append(", ");
            }
        }
        return allGroups.toString();
    }

}

