// Copyright 2015 The Bazel Authors. All rights reserved.
//
// 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
//
//    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 com.google.devtools.build.lib.packages.util;

import static com.google.common.truth.Truth.assertWithMessage;

import com.google.devtools.build.docgen.DocCheckerUtils;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.runtime.BlazeCommand;
import com.google.devtools.build.lib.runtime.BlazeCommandUtils;
import com.google.devtools.build.lib.runtime.BlazeModule;
import com.google.devtools.build.lib.runtime.BlazeRuntime;
import com.google.devtools.build.lib.runtime.BuiltinCommandModule;
import com.google.devtools.build.lib.runtime.ServerBuilder;
import com.google.devtools.common.options.Options;
import com.google.devtools.common.options.OptionsBase;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Utility functions for validating correctness of Bazel documentation.
 */
public abstract class DocumentationTestUtil {

  private DocumentationTestUtil() {}

  private static final Pattern CODE_FLAG_PATTERN =
      Pattern.compile(
          "<code class\\s*=\\s*[\"']flag[\"']\\s*>--([a-z_\\[\\]]*)<\\/code>",
          Pattern.CASE_INSENSITIVE);

  /**
   * Validates that a user manual {@code documentationSource} contains only the flags actually
   * provided by a given set of modules.
   */
  public static void validateUserManual(
      List<Class<? extends BlazeModule>> modules,
      ConfiguredRuleClassProvider ruleClassProvider,
      String documentationSource) throws Exception {
    // if there is a class missing, one can find it using
    //   find . -name "*.java" -exec grep -Hn "@Option(name = " {} \; | grep "xxx"
    // where 'xxx' is a flag name.
    List<BlazeModule> blazeModules = BlazeRuntime.createModules(modules);

    Map<String, Object> optionsMap = new HashMap<>();

    // collect all startup options
    for (Class<? extends OptionsBase> optionsClass :
        BlazeCommandUtils.getStartupOptions(blazeModules)) {
      optionsMap.putAll(Options.getDefaults(optionsClass).asMap());
    }

    // collect all command options
    ServerBuilder serverBuilder = new ServerBuilder();
    new BuiltinCommandModule().serverInit(null, serverBuilder);
    for (BlazeModule module : blazeModules) {
      module.serverInit(null, serverBuilder);
    }
    List<BlazeCommand> blazeCommands = serverBuilder.getCommands();

    for (BlazeCommand command : blazeCommands) {
      for (Class<? extends OptionsBase> optionClass :
          BlazeCommandUtils.getOptions(command.getClass(), blazeModules, ruleClassProvider)) {
        optionsMap.putAll(Options.getDefaults(optionClass).asMap());
      }
    }

    // check validity of option flags in manual
    Matcher anchorMatcher = CODE_FLAG_PATTERN.matcher(documentationSource);
    String flag;
    boolean found;

    while (anchorMatcher.find()) {
      flag = anchorMatcher.group(1);
      found = optionsMap.containsKey(flag);
      if (!found && flag.startsWith("no")) {
        found = optionsMap.containsKey(flag.substring(2));
      }
      if (!found && flag.startsWith("[no]")) {
        found = optionsMap.containsKey(flag.substring(4));
      }

      assertWithMessage("flag '" + flag + "' is not a blaze option (anymore)").that(found).isTrue();
    }

    String unclosedTag = DocCheckerUtils.getFirstUnclosedTagAndPrintHelp(documentationSource);
    assertWithMessage("Unclosed tag found: " + unclosedTag).that(unclosedTag).isNull();
  }
}
