// Copyright 2018 Google LLC
//
// 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.cloud.deploymentmanager.autogen;

import static com.google.common.base.Preconditions.checkArgument;

import com.google.cloud.deploymentmanager.autogen.proto.AcceleratorSpec;
import com.google.cloud.deploymentmanager.autogen.proto.ApplicationStatusSpec;
import com.google.cloud.deploymentmanager.autogen.proto.BooleanExpression;
import com.google.cloud.deploymentmanager.autogen.proto.BooleanExpression.BooleanDeployInputField;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.EmailBox;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.GroupedBooleanCheckbox;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.IntegerBox;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.IntegerDropdown;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.StringBox;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.StringDropdown;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputField.TypeCase;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputSection;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputSection.Placement;
import com.google.cloud.deploymentmanager.autogen.proto.DeployInputSpec;
import com.google.cloud.deploymentmanager.autogen.proto.DiskSpec;
import com.google.cloud.deploymentmanager.autogen.proto.ExternalIpSpec;
import com.google.cloud.deploymentmanager.autogen.proto.FirewallRuleSpec;
import com.google.cloud.deploymentmanager.autogen.proto.FirewallRuleSpec.TrafficSource;
import com.google.cloud.deploymentmanager.autogen.proto.GceMetadataItem;
import com.google.cloud.deploymentmanager.autogen.proto.GceMetadataItem.ValueSpecCase;
import com.google.cloud.deploymentmanager.autogen.proto.GceStartupScriptSpec;
import com.google.cloud.deploymentmanager.autogen.proto.GcpAuthScopeSpec;
import com.google.cloud.deploymentmanager.autogen.proto.ImageSpec;
import com.google.cloud.deploymentmanager.autogen.proto.InstanceUrlSpec;
import com.google.cloud.deploymentmanager.autogen.proto.MachineTypeSpec;
import com.google.cloud.deploymentmanager.autogen.proto.MultiVmDeploymentPackageSpec;
import com.google.cloud.deploymentmanager.autogen.proto.NetworkInterfacesSpec;
import com.google.cloud.deploymentmanager.autogen.proto.PasswordSpec;
import com.google.cloud.deploymentmanager.autogen.proto.PostDeployInfo;
import com.google.cloud.deploymentmanager.autogen.proto.PostDeployInfo.ConnectToInstanceSpec;
import com.google.cloud.deploymentmanager.autogen.proto.SingleVmDeploymentPackageSpec;
import com.google.cloud.deploymentmanager.autogen.proto.StackdriverSpec;
import com.google.cloud.deploymentmanager.autogen.proto.VmTierSpec;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;

/**
 * Validates complete specs.
 *
 * @see SpecDefaults
 */
final class SpecValidations {

  protected static final int INFO_ROW_MAX_LENGTH = 128;

  private static final ImmutableSet<String> METADATA_KEY_BLACKLIST =
      ImmutableSet.of(
          "status-config-url",
          "status-uptime-deadline",
          "status-variable-path",
          "startup-script",
          "startup-script-url");
  private static final ImmutableSet<Integer> VALID_GPU_COUNTS = ImmutableSet.of(0, 1, 2, 4, 8);

  private static final Pattern TIER_NAME_REGEX = Pattern.compile("[a-z0-9]+");

  private static final ImmutableSet<String> SUPPORTED_ACCELERATOR_TYPES =
      // LINT.IfChange(gpuTypes)
      ImmutableSet.of(
          "nvidia-tesla-k80",
          "nvidia-tesla-p100",
          "nvidia-tesla-v100",
          "nvidia-tesla-p100-vws",
          "nvidia-tesla-p4",
          "nvidia-tesla-p4-vws",
          "nvidia-tesla-t4",
          "nvidia-tesla-t4-vws",
          "nvidia-tesla-a100",
          "nvidia-a100-80gb",
          "nvidia-l4",
          "nvidia-l4-vws",
          "nvidia-h100-80gb");
  // LINT.ThenChange()

  private static final int MAX_NICS = 8;

  /** Validates that a spec is complete and reasonable. */
  public static void validate(SingleVmDeploymentPackageSpec input) {
    validateImages(input.getImagesList());
    validateBootDisk(input.getBootDisk());
    validateAdditionalDisks(input.getAdditionalDisksList());
    validateMachineType(input.getMachineType());
    validateNetworkInterfaces(input.getNetworkInterfaces());
    validateSingleVmFirewallRules(input.getFirewallRulesList());
    validateSingleVmPasswords(input.getPasswordsList());
    if (input.hasAdminUrl()) {
      validateSingleVmInstanceUrl(input.getAdminUrl(), "Admin URL");
    }
    if (input.hasSiteUrl()) {
      validateSingleVmInstanceUrl(input.getSiteUrl(), "Site URL");
    }
    validateGcpAuthScopes(input.getGcpAuthScopesList());
    if (input.hasGceStartupScript()) {
      validateStartupScript(input.getGceStartupScript());
    }
    if (input.hasApplicationStatus()) {
      validateApplicationStatus(input.getApplicationStatus());
    }
    if (input.hasPostDeploy()) {
      validateSingleVmPostDeployInfo(input.getPostDeploy());
    }
    if (input.hasDeployInput()) {
      validateDeployInput(input.getDeployInput());
    }
    if (input.hasStackdriver()) {
      validateStackdriver(input.getStackdriver());
    }
    validateSingleVmGceMetadataItems(input.getGceMetadataItemsList());
    validateMetadataKeyUniqueness(input);
    validateAccelerators(input.getAcceleratorsList());
  }

  /** Validates that a spec is complete and reasonable. */
  public static void validate(MultiVmDeploymentPackageSpec input) {
    checkArgument(input.getTiersCount() > 0, "At least one tier must be specified");
    for (VmTierSpec tier : input.getTiersList()) {
      checkArgument(
          TIER_NAME_REGEX.matcher(tier.getName()).matches(),
          "Tier must have a valid lowercased name");
      checkArgument(tier.getTitle().length() > 0, "Tier must have a valid title");
      validateImages(tier.getImagesList());
      validateBootDisk(tier.getBootDisk());
      validateAdditionalDisks(tier.getAdditionalDisksList());
      validateMachineType(tier.getMachineType());
      validateNetworkInterfaces(tier.getNetworkInterfaces());
      validateMultiVmFirewallRules(tier.getFirewallRulesList());
      validateGcpAuthScopes(tier.getGcpAuthScopesList());
      if (tier.hasGceStartupScript()) {
        validateStartupScript(tier.getGceStartupScript());
      }
      if (tier.hasApplicationStatus()) {
        validateApplicationStatus(tier.getApplicationStatus());
      }
      validateMultiVmGceMetadataItems(tier.getGceMetadataItemsList());
      validateAccelerators(tier.getAcceleratorsList());
    }
    validateMultiVmPasswords(input.getPasswordsList());
    if (input.hasAdminUrl()) {
      validateMultiVmInstanceUrl(input.getAdminUrl(), "Admin URL");
    }
    if (input.hasSiteUrl()) {
      validateMultiVmInstanceUrl(input.getSiteUrl(), "Site URL");
    }
    if (input.hasPostDeploy()) {
      validateMultiVmPostDeployInfo(input.getPostDeploy());
    }
    if (input.hasDeployInput()) {
      validateDeployInput(input.getDeployInput());
    }
    if (input.hasStackdriver()) {
      validateStackdriver(input.getStackdriver());
    }
    validateMetadataKeyUniqueness(input);
  }

  private static void validateCommonDiskOptions(DiskSpec input) {
    checkArgument(input.hasDiskSize(), "Disk size must be specified");
    checkArgument(
        input.getDiskSize().getDefaultSizeGb() > 0, "A valid default disk size must be specified");
    checkArgument(input.hasDiskType(), "Disk type must be specified");
    checkArgument(
        !input.getDiskType().getDefaultType().isEmpty(),
        "A valid default disk type must be specified");
    checkArgument(
        !input.getDisplayLabel().isEmpty(), "A valid disk display label must be specified");
  }

  private static void validateBootDisk(DiskSpec input) {
    validateCommonDiskOptions(input);
  }

  private static void validateAdditionalDisks(List<DiskSpec> input) {
    for (DiskSpec disk : input) {
      validateCommonDiskOptions(disk);
      checkArgument(disk.hasDeviceNameSuffix(), "Disk must have a device name specified");
    }
  }

  private static void validateSingleVmFirewallRules(List<FirewallRuleSpec> firewallRules) {
    validateCommonFirewallRules(firewallRules);
    for (FirewallRuleSpec rule : firewallRules) {
      checkArgument(
          rule.getAllowedSource() == TrafficSource.PUBLIC,
          "SingleVM only supports PUBLIC firewall rules");
    }
  }

  private static void validateMultiVmFirewallRules(List<FirewallRuleSpec> firewallRules) {
    validateCommonFirewallRules(firewallRules);
  }

  private static void validateCommonFirewallRules(List<FirewallRuleSpec> firewallRules) {
    for (FirewallRuleSpec rule : firewallRules) {
      checkArgument(
          rule.getAllowedSource() != TrafficSource.SOURCE_UNSPECIFIED,
          "A firewall rule must have a valid source");
      checkArgument(
          rule.getProtocol() != FirewallRuleSpec.Protocol.PROTOCOL_UNSPECIFIED,
          "A firewall rule must have a valid protocol");
      checkArgument(
          !rule.getProtocol().equals(FirewallRuleSpec.Protocol.ICMP) || rule.getPort().isEmpty(),
          "ICMP firewall rule must not specify a port");
    }
  }

  private static void validateGcpAuthScopes(List<GcpAuthScopeSpec> gcpAuthScopes) {
    Set<GcpAuthScopeSpec.Scope> seenScopes = new HashSet<>();
    for (GcpAuthScopeSpec spec : gcpAuthScopes) {
      checkArgument(
          spec.getScope() != GcpAuthScopeSpec.Scope.SCOPE_UNSPECIFIED,
          "The GcpAuthScopeSpec must have a valid scope value");
      checkArgument(
          seenScopes.add(spec.getScope()),
          "Duplicate scope in GcpAuthScopeSpec list: " + spec.getScope());
    }
  }

  private static void validateImages(List<ImageSpec> imageSpecs) {
    checkArgument(imageSpecs.size() >= 1, "At least one image must be specified");
    for (ImageSpec imageSpec : imageSpecs) {
      validateImage(imageSpec);
    }
  }

  private static void validateImage(ImageSpec imageSpec) {
    checkArgument(!imageSpec.getName().isEmpty(), "Image must have a valid name");
    checkArgument(!imageSpec.getProject().isEmpty(), "Image must have a valid project");
  }

  private static void validateSingleVmInstanceUrl(@Nullable InstanceUrlSpec spec, String urlName) {
    validateCommonInstanceUrl(spec, urlName);
  }

  private static void validateMultiVmInstanceUrl(@Nullable InstanceUrlSpec spec, String urlName) {
    if (spec == null) {
      return;
    }
    validateCommonInstanceUrl(spec, urlName);
    checkArgument(spec.hasTierVm(), "A tier VM is required for " + urlName);
    checkArgument(
        spec.getTierVm().getTier().length() > 0, "Tier VM must have a valid tier for " + urlName);
  }

  private static void validateCommonInstanceUrl(@Nullable InstanceUrlSpec spec, String urlName) {
    if (spec == null) {
      return;
    }
    checkArgument(
        spec.getScheme() != InstanceUrlSpec.Scheme.SCHEME_UNSPECIFIED,
        "A scheme is required for %s",
        urlName);
  }

  private static void validateSingleVmInstanceConnectSpec(
      ConnectToInstanceSpec spec, PostDeployInfo postDeployInfo) {
    validateCommonInstanceConnectSpec(spec, postDeployInfo);

    checkArgument(!spec.hasTierVm(), "Tier VM must not be specified for a single vm spec");
  }

  private static void validateMultiVmInstanceConnectSpec(
      ConnectToInstanceSpec spec, PostDeployInfo postDeployInfo) {
    validateCommonInstanceConnectSpec(spec, postDeployInfo);

    checkArgument(
        spec.hasTierVm() && !Strings.isNullOrEmpty(spec.getTierVm().getTier()),
        "Multi-vm connect button spec must specify valid tier name");
  }

  private static void validateCommonInstanceConnectSpec(
      ConnectToInstanceSpec spec, PostDeployInfo postDeployInfo) {
    // Keep support for deprecated attribute of connect_button_label.
    // If the Autogen spec has different values specified for both, the display_label of
    // the connect button and connect_button_label of post deploy info, it should fail.
    checkArgument(
        Strings.isNullOrEmpty(spec.getDisplayLabel())
            || Strings.isNullOrEmpty(postDeployInfo.getConnectButtonLabel())
            || spec.getDisplayLabel().equals(postDeployInfo.getConnectButtonLabel()),
        "At most only one of connect button's display_label and post deploy's "
            + "connect_button_label can be specified, or they must have the same value");
  }

  private static void validateMachineType(MachineTypeSpec machineType) {
    checkArgument(
        !machineType.getDefaultMachineType().getGceMachineType().isEmpty(),
        "Default machine type name must be valid");
    if (machineType.hasMinimum()) {
      // If unset, these values will be zero (which is allowed)
      checkArgument(machineType.getMinimum().getCpu() >= 0, "Minimum CPUs must be nonnegative");
      checkArgument(machineType.getMinimum().getRamGb() >= 0, "Minimum RAM must be nonnegative");
    }
  }

  private static void validateNetworkInterfaces(NetworkInterfacesSpec spec) {
    int min = spec.getMinCount();
    int max = spec.getMaxCount();
    checkArgument(min > 0, "Minimum number of Network interfaces must be greater than 0.");
    checkArgument(
        max >= min && max <= MAX_NICS,
        String.format(
            "Maxmium number of Network interfaces must be greater than minimum and at most %d.",
            MAX_NICS));
    checkArgument(
        spec.getLabelsCount() <= spec.getMinCount() + 1,
        "The number of labels must not exceed min_count + 1.");
    validateExternalIp(spec.getExternalIp());
  }

  @VisibleForTesting
  static void validateExternalIp(ExternalIpSpec externalIp) {
    checkArgument(
        externalIp.getDefaultType() != ExternalIpSpec.Type.TYPE_UNSPECIFIED,
        "External IP default type must have a valid type");
  }

  @VisibleForTesting
  static void validateSingleVmPasswords(List<PasswordSpec> passwords) {
    for (PasswordSpec password : passwords) {
      validateCommonPassword(password);
      if (password.hasGenerateIf()) {
        validateSingleVmBooleanExpression(password.getGenerateIf());
      }
    }
  }

  @VisibleForTesting
  static void validateMultiVmPasswords(List<PasswordSpec> passwords) {
    for (PasswordSpec password : passwords) {
      validateCommonPassword(password);
      if (password.hasGenerateIf()) {
        validateMultiVmBooleanExpression(password.getGenerateIf());
      }
    }
  }

  private static void validateCommonPassword(PasswordSpec password) {
    checkArgument(!password.getMetadataKey().isEmpty(), "Password must have a valid metadata key");
    checkArgument(password.getLength() > 0, "Password must have a valid length");
    checkArgument(
        !password.getDisplayLabel().isEmpty(), "Password must have a valid display label");
    checkArgument(
        !METADATA_KEY_BLACKLIST.contains(password.getMetadataKey()),
        "Password metadata key cannot be one of " + METADATA_KEY_BLACKLIST);
  }

  @VisibleForTesting
  static void validateApplicationStatus(ApplicationStatusSpec appStatus) {
    if (appStatus.getType() == ApplicationStatusSpec.StatusType.WAITER) {
      checkArgument(
          appStatus.hasWaiter(), "Application status of type WAITER must have a valid waiter spec");
      checkArgument(
          appStatus.getWaiter().getWaiterTimeoutSecs() > 0,
          "Application status waiter must have a valid timeout");
      if (appStatus.getWaiter().hasScript()) {
        checkArgument(
            appStatus.getWaiter().getScript().getCheckTimeoutSecs() > 0,
            "Application status waiter script must have a valid timeout");
      }
    } else {
      checkArgument(
          !appStatus.hasWaiter(),
          "Waiter spec should only be set for an application status of type WAITER");
    }
  }

  @VisibleForTesting
  static void validateSingleVmPostDeployInfo(PostDeployInfo postDeployInfo) {
    for (PostDeployInfo.ActionItem item : postDeployInfo.getActionItemsList()) {
      validateCommonActionItem(item);
      if (item.hasShowIf()) {
        validateSingleVmBooleanExpression(item.getShowIf());
      }
    }

    for (PostDeployInfo.InfoRow row : postDeployInfo.getInfoRowsList()) {
      validateCommonInfoRow(row);
      if (row.hasShowIf()) {
        validateSingleVmBooleanExpression(row.getShowIf());
      }
    }

    if (postDeployInfo.hasConnectButton()) {
      validateSingleVmInstanceConnectSpec(postDeployInfo.getConnectButton(), postDeployInfo);
    }
  }

  @VisibleForTesting
  static void validateMultiVmPostDeployInfo(PostDeployInfo postDeployInfo) {
    for (PostDeployInfo.ActionItem item : postDeployInfo.getActionItemsList()) {
      validateCommonActionItem(item);
      if (item.hasShowIf()) {
        validateMultiVmBooleanExpression(item.getShowIf());
      }
    }

    for (PostDeployInfo.InfoRow row : postDeployInfo.getInfoRowsList()) {
      validateCommonInfoRow(row);
      if (row.hasShowIf()) {
        validateMultiVmBooleanExpression(row.getShowIf());
      }
    }

    if (postDeployInfo.hasConnectButton()) {
      validateMultiVmInstanceConnectSpec(postDeployInfo.getConnectButton(), postDeployInfo);
    }
  }

  private static void validateCommonActionItem(PostDeployInfo.ActionItem item) {
    checkArgument(
        !item.getHeading().isEmpty(), "Post deploy action item must have a valid heading");
    checkArgument(
        !item.getDescription().isEmpty() || !item.getSnippet().isEmpty(),
        "Post deploy action item must have at least description or snippet");
  }

  private static void validateCommonInfoRow(PostDeployInfo.InfoRow row) {
    switch (row.getValueSpecCase()) {
      case VALUE:
        checkArgument(
            row.getValue().length() <= INFO_ROW_MAX_LENGTH,
            String.format(
                "Info row value length cannot exceed the limit of %d characters",
                INFO_ROW_MAX_LENGTH));
        break;
      case VALUE_FROM_DEPLOY_INPUT_FIELD:
        checkArgument(
            !Strings.isNullOrEmpty(row.getValueFromDeployInputField()),
            "Expected non-empty deploy input field name for info row value");
        break;
      default:
        throw new IllegalArgumentException("Unexpected info row value");
    }
  }

  @VisibleForTesting
  static void validateSingleVmBooleanExpression(BooleanExpression spec) {
    validateCommonBooleanExpression(spec);
    if (spec.hasHasExternalIp()) {
      checkArgument(
          Strings.isNullOrEmpty(spec.getHasExternalIp().getTier()),
          "Tier attribute must not be specified for has_external_ip boolean "
              + "expression in a single VM's spec");
    }
  }

  @VisibleForTesting
  static void validateMultiVmBooleanExpression(BooleanExpression spec) {
    validateCommonBooleanExpression(spec);
    if (spec.hasHasExternalIp()) {
      checkArgument(
          !Strings.isNullOrEmpty(spec.getHasExternalIp().getTier()),
          "Tier attribute must be specified for has_external_ip boolean expression "
              + "in a multi VM's spec");
    }
  }

  private static void validateCommonBooleanExpression(BooleanExpression spec) {
    if (spec.hasBooleanDeployInputField()) {
      BooleanDeployInputField field = spec.getBooleanDeployInputField();
      checkArgument(
          !field.getName().isEmpty(),
          "Boolean expression for deploy input field must specify the field's name");
    }
  }

  @VisibleForTesting
  static void validateSingleVmGceMetadataItems(List<GceMetadataItem> items) {
    for (GceMetadataItem item : items) {
      checkArgument(!item.getKey().isEmpty(), "GCE metadata item must have a valid key");
      checkArgument(
          !METADATA_KEY_BLACKLIST.contains(item.getKey()),
          "GCE metadata item key cannot be one of " + METADATA_KEY_BLACKLIST);
      checkArgument(
          item.getValueSpecCase() != ValueSpecCase.VALUESPEC_NOT_SET,
          "GCE metadata item must have a valid value");
      checkArgument(
          item.getValueSpecCase() != ValueSpecCase.TIER_VM_NAMES,
          "GCE metadata item for single VM cannot specify tier VM");
    }
  }

  @VisibleForTesting
  static void validateMultiVmGceMetadataItems(List<GceMetadataItem> items) {
    for (GceMetadataItem item : items) {
      checkArgument(!item.getKey().isEmpty(), "GCE metadata item must have a valid key");
      checkArgument(
          !METADATA_KEY_BLACKLIST.contains(item.getKey()),
          "GCE metadata item key cannot be one of " + METADATA_KEY_BLACKLIST);
      checkArgument(
          item.getValueSpecCase() != ValueSpecCase.VALUESPEC_NOT_SET,
          "GCE metadata item must have a valid value");
    }
  }

  private static void validateMetadataKeyUniqueness(SingleVmDeploymentPackageSpec spec) {
    // Ensures that metadata keys are unique.
    Multiset<String> metadataKeyCounts = HashMultiset.create();
    for (PasswordSpec password : spec.getPasswordsList()) {
      metadataKeyCounts.add(password.getMetadataKey());
    }
    for (GceMetadataItem metadataItem : spec.getGceMetadataItemsList()) {
      metadataKeyCounts.add(metadataItem.getKey());
    }
    for (Multiset.Entry<String> entry : metadataKeyCounts.entrySet()) {
      if (entry.getCount() > 1) {
        throw new IllegalArgumentException(
            String.format("Metadata key '%s' is not unique", entry.getElement()));
      }
    }
  }

  private static void validateMetadataKeyUniqueness(MultiVmDeploymentPackageSpec spec) {
    // Ensures that metadata keys are unique.
    Multiset<String> metadataKeyCounts = HashMultiset.create();
    for (PasswordSpec password : spec.getPasswordsList()) {
      metadataKeyCounts.add(password.getMetadataKey());
    }
    for (VmTierSpec tier : spec.getTiersList()) {
      Multiset<String> perTier = HashMultiset.create(metadataKeyCounts);
      for (GceMetadataItem metadataItem : tier.getGceMetadataItemsList()) {
        perTier.add(metadataItem.getKey());
      }
      for (Multiset.Entry<String> entry : perTier.entrySet()) {
        if (entry.getCount() > 1) {
          throw new IllegalArgumentException(
              String.format("Metadata key '%s' is not unique", entry.getElement()));
        }
      }
    }
  }

  @VisibleForTesting
  static void validateAccelerators(List<AcceleratorSpec> accelerators) {
    if (accelerators.isEmpty()) {
      return;
    }
    checkArgument(accelerators.size() <= 1, "At most one accelerator allowed.");

    AcceleratorSpec accelerator = accelerators.get(0);
    checkArgument(
        accelerator.getTypesList().size() >= 1, "Accelerators must have at least one type.");
    Set<String> gpuTypes = ImmutableSet.copyOf(accelerator.getTypesList());
    Set<String> unsupportedTypes = Sets.difference(gpuTypes, SUPPORTED_ACCELERATOR_TYPES);
    checkArgument(
        unsupportedTypes.isEmpty(), "Unsupported accelerator types: %s", unsupportedTypes);
    checkArgument(accelerator.getMinCount() >= 0, "Accelerator min count must not be negative.");
    checkArgument(
        VALID_GPU_COUNTS.contains(accelerator.getMinCount()),
        "Accelerator min count must be one of: %s",
        VALID_GPU_COUNTS.toString());
    if (accelerator.getMaxCount() != 0) {
      checkArgument(accelerator.getMaxCount() > 0, "Accelerator max count must be greater than 0.");
      checkArgument(
          accelerator.getMinCount() <= accelerator.getMaxCount(),
          "Accelerator min count must not be greater than max count.");
      checkArgument(
          VALID_GPU_COUNTS.contains(accelerator.getMaxCount()),
          "Accelerator max count must be one of: %s",
          VALID_GPU_COUNTS.toString());
    }
    if (accelerator.getDefaultCount() != 0) {
      checkArgument(
          accelerator.getDefaultCount() >= 0, "Accelerator default count must not be negative.");
      checkArgument(
          accelerator.getDefaultCount() >= accelerator.getMinCount(),
          "Accelerator default count must not be less than min count.");
      if (accelerator.getMaxCount() != 0) {
        checkArgument(
            accelerator.getDefaultCount() <= accelerator.getMaxCount(),
            "Accelerator default count must not be greater than max count.");
      }
      checkArgument(
          VALID_GPU_COUNTS.contains(accelerator.getDefaultCount()),
          "Accelerator default count must be one of: %s",
          VALID_GPU_COUNTS.toString());
    }
    if (!accelerator.getDefaultType().isEmpty()) {
      checkArgument(
          gpuTypes.contains(accelerator.getDefaultType()),
          "Default Accelerator Type must be one of %s",
          accelerator.getTypesList());
    }
  }

  @VisibleForTesting
  static void validateDeployInput(DeployInputSpec spec) {
    Set<String> runningSectionNames = new HashSet<>();
    Set<String> runningFieldNames = new HashSet<>();
    Set<String> runningDisplayGroups = new HashSet<>();
    for (DeployInputSection section : spec.getSectionsList()) {
      checkArgument(
          section.getPlacement() != Placement.PLACEMENT_UNSPECIFIED,
          "Deploy input section must have a valid placement");
      switch (section.getPlacement()) {
        case MAIN:
          break;
        case TIER:
          checkArgument(
              section.getTier().length() > 0, "Tier input section must have a valid tier name");
          break;
        default:
          checkArgument(
              !section.getName().isEmpty(), "Custom deploy input section must have valid name");
          if (!runningSectionNames.add(section.getName())) {
            throw new IllegalArgumentException(
                "Deploy input sections with the same name: " + section.getName());
          }
          checkArgument(
              !section.getTitle().isEmpty(), "Custom deploy input section must have a valid title");
      }
      checkArgument(
          section.getFieldsCount() > 0, "Deploy input section must have at least 1 field");
      validateDeployInputFields(section.getFieldsList(), runningFieldNames, runningDisplayGroups);
    }
  }

  @VisibleForTesting
  static void validateStartupScript(GceStartupScriptSpec startupScript) {
    checkArgument(
        !Strings.isNullOrEmpty(startupScript.getBashScriptContent()),
        "Startup script must specify non-empty script content");
  }

  @VisibleForTesting
  static void validateDeployInputFields(
      List<DeployInputField> fields,
      Set<String> runningFieldNames,
      Set<String> runningDisplayGroups) {
    boolean isInBooleanGroup = false;
    for (DeployInputField field : fields) {
      checkArgument(!field.getName().isEmpty(), "Deploy input field must have a valid name");
      if (!runningFieldNames.add(field.getName())) {
        throw new IllegalArgumentException(
            "Deploy input fields with the same name: " + field.getName());
      }
      checkArgument(!field.getTitle().isEmpty(), "Deploy input field must have a valid title");

      switch (field.getTypeCase()) {
        case BOOLEAN_CHECKBOX:
          break;
        case GROUPED_BOOLEAN_CHECKBOX:
          {
            GroupedBooleanCheckbox checkbox = field.getGroupedBooleanCheckbox();
            if (!isInBooleanGroup) {
              checkArgument(
                  checkbox.hasDisplayGroup(),
                  String.format(
                      "The first grouped boolean checkbox '%s' must have a display group",
                      field.getName()));
            }
            if (checkbox.hasDisplayGroup()) {
              GroupedBooleanCheckbox.DisplayGroup displayGroup = checkbox.getDisplayGroup();
              checkArgument(
                  !displayGroup.getName().isEmpty(),
                  String.format("Field '%s' has a display group without a name", field.getName()));
              if (!runningDisplayGroups.add(displayGroup.getName())) {
                throw new IllegalArgumentException(
                    "Display groups with the same name: " + displayGroup.getName());
              }
              checkArgument(
                  !displayGroup.getTitle().isEmpty(),
                  String.format("Display group '%s' must have a title", displayGroup.getName()));
            }
            break;
          }
        case INTEGER_BOX:
          {
            if (field.getRequired()) {
              IntegerBox box = field.getIntegerBox();
              checkArgument(
                  box.hasDefaultValue() || box.hasTestDefaultValue(),
                  String.format(
                      "Field '%s' is required - it should have defaultValue or testDefaultValue",
                      field.getName()));
            }
            break;
          }
        case INTEGER_DROPDOWN:
          {
            IntegerDropdown dropdown = field.getIntegerDropdown();
            checkArgument(
                dropdown.getValuesCount() > 0,
                String.format("Field '%s' must have at least 1 value", field.getName()));
            if (dropdown.hasDefaultValueIndex()) {
              checkArgument(
                  dropdown.getDefaultValueIndex().getValue() < dropdown.getValuesCount(),
                  String.format(
                      "Invalid default index %d while there are only %d values in field '%s'",
                      dropdown.getDefaultValueIndex().getValue(),
                      dropdown.getValuesCount(),
                      field.getName()));
            }
            break;
          }
        case STRING_BOX:
          {
            if (field.getRequired()) {
              StringBox box = field.getStringBox();
              boolean hasDefaultValue = !Strings.isNullOrEmpty(box.getDefaultValue());
              boolean hasTestDefaultValue = !Strings.isNullOrEmpty(box.getTestDefaultValue());
              checkArgument(
                  hasDefaultValue || hasTestDefaultValue,
                  String.format(
                      "Field '%s' is required - it should have defaultValue or testDefaultValue",
                      field.getName()));
            }
            break;
          }
        case STRING_DROPDOWN:
          {
            StringDropdown dropdown = field.getStringDropdown();
            checkArgument(
                dropdown.getValuesCount() > 0,
                String.format("Field '%s' must have at least 1 value", field.getName()));
            if (dropdown.hasDefaultValueIndex()) {
              checkArgument(
                  dropdown.getDefaultValueIndex().getValue() < dropdown.getValuesCount(),
                  String.format(
                      "Invalid default index %d while there are only %d values in field '%s'",
                      dropdown.getDefaultValueIndex().getValue(),
                      dropdown.getValuesCount(),
                      field.getName()));
            }
            break;
          }
        case ZONE_DROPDOWN:
          break;
        case EMAIL_BOX:
          if (field.getRequired()) {
            EmailBox box = field.getEmailBox();
            // EmailBox should always have a testDefaultValue.
            // It's either being specified by defaultValue,
            // explicitly by testDefaultValue, or being filled in by defaults.
            boolean hasTestDefaultValue = !box.getTestDefaultValue().isEmpty();
            checkArgument(
                hasTestDefaultValue,
                "Field '%s' is required - it should have testDefaultValue",
                field.getName());
          }
          break;
        case TYPE_NOT_SET:
          throw new IllegalArgumentException("Deploy input field must be of exactly one type");
      }

      isInBooleanGroup = field.getTypeCase() == TypeCase.GROUPED_BOOLEAN_CHECKBOX;
    }
  }

  @VisibleForTesting
  static void validateStackdriver(StackdriverSpec stackdriver) {
    checkArgument(
        stackdriver.hasMonitoring() || stackdriver.hasLogging(),
        "Invalid Stackdriver spec. At least one of logging or monitoring must be specified");
  }

  private SpecValidations() {}
}
