/*
 * 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.beam.sdk.options;

import static java.util.Locale.ROOT;
import static org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Maps.uniqueIndex;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.isEmptyString;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeFalse;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.google.auto.service.AutoService;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.beam.model.jobmanagement.v1.JobApi.PipelineOptionDescriptor;
import org.apache.beam.model.jobmanagement.v1.JobApi.PipelineOptionType;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.PipelineRunner;
import org.apache.beam.sdk.runners.PipelineRunnerRegistrar;
import org.apache.beam.sdk.testing.CrashingRunner;
import org.apache.beam.sdk.testing.ExpectedLogs;
import org.apache.beam.sdk.testing.InterceptingUrlClassLoader;
import org.apache.beam.sdk.testing.RestoreSystemProperties;
import org.apache.beam.sdk.util.common.ReflectHelpers;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ArrayListMultimap;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Collections2;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableList;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ImmutableMap;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.ListMultimap;
import org.apache.beam.vendor.guava.v32_1_2_jre.com.google.common.collect.Sets;
import org.apache.commons.lang3.SystemUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link PipelineOptionsFactory}. */
@RunWith(JUnit4.class)
@SuppressWarnings({
  "rawtypes", // TODO(https://github.com/apache/beam/issues/20447)
})
public class PipelineOptionsFactoryTest {
  private static final String DEFAULT_RUNNER_NAME = "DirectRunner";
  private static final Class<? extends PipelineRunner<?>> REGISTERED_RUNNER =
      RegisteredTestRunner.class;

  @Rule public ExpectedException expectedException = ExpectedException.none();
  @Rule public TestRule restoreSystemProperties = new RestoreSystemProperties();
  @Rule public ExpectedLogs expectedLogs = ExpectedLogs.none(PipelineOptionsFactory.class);

  @Test
  public void testRevision() {
    PipelineOptions options = PipelineOptionsFactory.create();
    assertEquals(1, options.revision());
    for (int i = 0; i < 10; i++) {
      options.setJobName("other" + i);
      // updates are idempotent, the 2nd call won't increment the revision
      options.setJobName("other" + i);
    }
    assertEquals(11, options.revision());
  }

  @Test
  public void testAutomaticRegistrationOfPipelineOptions() {
    assertTrue(PipelineOptionsFactory.getRegisteredOptions().contains(RegisteredTestOptions.class));
  }

  @Test
  public void testAutomaticRegistrationOfRunners() {
    assertEquals(
        REGISTERED_RUNNER,
        PipelineOptionsFactory.getRegisteredRunners()
            .get(REGISTERED_RUNNER.getSimpleName().toLowerCase()));
  }

  @Test
  public void testAutomaticRegistrationInculdesWithoutRunnerSuffix() {
    // Sanity check to make sure the substring works appropriately
    assertEquals(
        "RegisteredTest",
        REGISTERED_RUNNER
            .getSimpleName()
            .substring(0, REGISTERED_RUNNER.getSimpleName().length() - "Runner".length()));
    Map<String, Class<? extends PipelineRunner<?>>> registered =
        PipelineOptionsFactory.CACHE.get().getSupportedPipelineRunners();
    assertEquals(
        REGISTERED_RUNNER,
        registered.get(
            REGISTERED_RUNNER
                .getSimpleName()
                .toLowerCase()
                .substring(0, REGISTERED_RUNNER.getSimpleName().length() - "Runner".length())));
  }

  @Test
  public void testAppNameIsSet() {
    ApplicationNameOptions options = PipelineOptionsFactory.as(ApplicationNameOptions.class);
    assertEquals(PipelineOptionsFactoryTest.class.getSimpleName(), options.getAppName());
  }

  /** A simple test interface. */
  public interface TestPipelineOptions extends PipelineOptions {
    String getTestPipelineOption();

    void setTestPipelineOption(String value);
  }

  @Test
  public void testAppNameIsSetWhenUsingAs() {
    TestPipelineOptions options = PipelineOptionsFactory.as(TestPipelineOptions.class);
    assertEquals(
        PipelineOptionsFactoryTest.class.getSimpleName(),
        options.as(ApplicationNameOptions.class).getAppName());
  }

  @Test
  public void testOptionsIdIsSet() throws Exception {
    ObjectMapper mapper =
        new ObjectMapper()
            .registerModules(ObjectMapper.findModules(ReflectHelpers.findClassLoader()));
    PipelineOptions options = PipelineOptionsFactory.create();
    // We purposely serialize/deserialize to get another instance. This allows to test if the
    // default has been set or not.
    PipelineOptions clone =
        mapper.readValue(mapper.writeValueAsString(options), PipelineOptions.class);
    // It is important that we don't call getOptionsId() before we have created the clone.
    assertEquals(options.getOptionsId(), clone.getOptionsId());
  }

  @Test
  public void testManualRegistration() {
    assertFalse(PipelineOptionsFactory.getRegisteredOptions().contains(TestPipelineOptions.class));
    PipelineOptionsFactory.register(TestPipelineOptions.class);
    assertTrue(PipelineOptionsFactory.getRegisteredOptions().contains(TestPipelineOptions.class));
  }

  @Test
  public void testDefaultRegistration() {
    assertTrue(PipelineOptionsFactory.getRegisteredOptions().contains(PipelineOptions.class));
  }

  /** A test interface missing a getter. */
  public interface MissingGetter extends PipelineOptions {
    void setObject(Object value);
  }

  @Test
  public void testMissingGetterThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected getter for property [object] of type [java.lang.Object] on "
            + "[org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MissingGetter].");

    PipelineOptionsFactory.as(MissingGetter.class);
  }

  /** A test interface missing multiple getters. */
  public interface MissingMultipleGetters extends MissingGetter {
    void setOtherObject(Object value);
  }

  @Test
  public void testMultipleMissingGettersThrows() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "missing property methods on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MissingMultipleGetters]");
    expectedException.expectMessage("getter for property [object] of type [java.lang.Object]");
    expectedException.expectMessage("getter for property [otherObject] of type [java.lang.Object]");

    PipelineOptionsFactory.as(MissingMultipleGetters.class);
  }

  /** A test interface missing a setter. */
  public interface MissingSetter extends PipelineOptions {
    Object getObject();
  }

  @Test
  public void testMissingSetterThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected setter for property [object] of type [java.lang.Object] on "
            + "[org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MissingSetter].");

    PipelineOptionsFactory.as(MissingSetter.class);
  }

  /** A test interface missing multiple setters. */
  public interface MissingMultipleSetters extends MissingSetter {
    Object getOtherObject();
  }

  @Test
  public void testMissingMultipleSettersThrows() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "missing property methods on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MissingMultipleSetters]");
    expectedException.expectMessage("setter for property [object] of type [java.lang.Object]");
    expectedException.expectMessage("setter for property [otherObject] of type [java.lang.Object]");

    PipelineOptionsFactory.as(MissingMultipleSetters.class);
  }

  /** A test interface missing a setter and a getter. */
  public interface MissingGettersAndSetters extends MissingGetter {
    Object getOtherObject();
  }

  @Test
  public void testMissingGettersAndSettersThrows() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "missing property methods on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MissingGettersAndSetters]");
    expectedException.expectMessage("getter for property [object] of type [java.lang.Object]");
    expectedException.expectMessage("setter for property [otherObject] of type [java.lang.Object]");

    PipelineOptionsFactory.as(MissingGettersAndSetters.class);
  }

  /** A test interface with a type mismatch between the getter and setter. */
  public interface GetterSetterTypeMismatch extends PipelineOptions {
    boolean getValue();

    void setValue(int value);
  }

  @Test
  public void testGetterSetterTypeMismatchThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Type mismatch between getter and setter methods for property [value]. Getter is of type "
            + "[boolean] whereas setter is of type [int].");

    PipelineOptionsFactory.as(GetterSetterTypeMismatch.class);
  }

  /** A test interface with multiple type mismatches between getters and setters. */
  public interface MultiGetterSetterTypeMismatch extends GetterSetterTypeMismatch {
    long getOther();

    void setOther(String other);
  }

  @Test
  public void testMultiGetterSetterTypeMismatchThrows() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Type mismatches between getters and setters detected:");
    expectedException.expectMessage(
        "Property [value]: Getter is of type " + "[boolean] whereas setter is of type [int].");
    expectedException.expectMessage(
        "Property [other]: Getter is of type [long] "
            + "whereas setter is of type [class java.lang.String].");
    PipelineOptionsFactory.as(MultiGetterSetterTypeMismatch.class);
  }

  /** A test interface representing a composite interface. */
  public interface CombinedObject extends MissingGetter, MissingSetter {}

  @Test
  public void testHavingSettersGettersFromSeparateInterfacesIsValid() {
    PipelineOptionsFactory.as(CombinedObject.class);
  }

  /** A test interface that contains a non-bean style method. */
  public interface ExtraneousMethod extends PipelineOptions {
    String extraneousMethod(int value, String otherValue);
  }

  @Test
  public void testHavingExtraneousMethodThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Methods [extraneousMethod(int, String)] on "
            + "[org.apache.beam.sdk.options.PipelineOptionsFactoryTest$ExtraneousMethod] "
            + "do not conform to being bean properties.");

    PipelineOptionsFactory.as(ExtraneousMethod.class);
  }

  /** A test interface that has a conflicting return type with its parent. */
  public interface ReturnTypeConflict extends CombinedObject {
    @Override
    String getObject();

    void setObject(String value);
  }

  @Test
  public void testReturnTypeConflictThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Method [getObject] has multiple definitions [public abstract java.lang.Object "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MissingSetter"
            + ".getObject(), public abstract java.lang.String "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$ReturnTypeConflict"
            + ".getObject()] with different return types for ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$ReturnTypeConflict].");
    PipelineOptionsFactory.as(ReturnTypeConflict.class);
  }

  /** An interface to provide multiple methods with return type conflicts. */
  public interface MultiReturnTypeConflictBase extends CombinedObject {
    Object getOther();

    void setOther(Object object);
  }

  /** A test interface that has multiple conflicting return types with its parent. */
  public interface MultiReturnTypeConflict extends MultiReturnTypeConflictBase {
    @Override
    String getObject();

    void setObject(String value);

    @Override
    Long getOther();

    void setOther(Long other);
  }

  @Test
  public void testMultipleReturnTypeConflictsThrows() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "[org.apache.beam.sdk.options." + "PipelineOptionsFactoryTest$MultiReturnTypeConflict]");
    expectedException.expectMessage(
        "Methods with multiple definitions with different return types");
    expectedException.expectMessage("Method [getObject] has multiple definitions");
    expectedException.expectMessage(
        "public abstract java.lang.Object "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$"
            + "MissingSetter.getObject()");
    expectedException.expectMessage(
        "public abstract java.lang.String org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MultiReturnTypeConflict.getObject()");
    expectedException.expectMessage("Method [getOther] has multiple definitions");
    expectedException.expectMessage(
        "public abstract java.lang.Object "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$"
            + "MultiReturnTypeConflictBase.getOther()");
    expectedException.expectMessage(
        "public abstract java.lang.Long org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MultiReturnTypeConflict.getOther()");

    PipelineOptionsFactory.as(MultiReturnTypeConflict.class);
  }

  /** Test interface that has {@link JsonIgnore @JsonIgnore} on a setter for a property. */
  public interface SetterWithJsonIgnore extends PipelineOptions {
    String getValue();

    @JsonIgnore
    void setValue(String value);
  }

  @Test
  public void testSetterAnnotatedWithJsonIgnore() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected setter for property [value] to not be marked with @JsonIgnore on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$SetterWithJsonIgnore]");
    PipelineOptionsFactory.as(SetterWithJsonIgnore.class);
  }

  /** Test interface that has {@link JsonIgnore @JsonIgnore} on multiple setters. */
  public interface MultiSetterWithJsonIgnore extends SetterWithJsonIgnore {
    Integer getOther();

    @JsonIgnore
    void setOther(Integer other);
  }

  @Test
  public void testMultipleSettersAnnotatedWithJsonIgnore() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Found setters marked with @JsonIgnore:");
    expectedException.expectMessage(
        "property [other] should not be marked with @JsonIgnore on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiSetterWithJsonIgnore]");
    expectedException.expectMessage(
        "property [value] should not be marked with @JsonIgnore on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$SetterWithJsonIgnore]");
    PipelineOptionsFactory.as(MultiSetterWithJsonIgnore.class);
  }

  /**
   * This class is has a conflicting field with {@link CombinedObject} that doesn't have {@link
   * JsonIgnore @JsonIgnore}.
   */
  public interface GetterWithJsonIgnore extends PipelineOptions {
    @JsonIgnore
    Object getObject();

    void setObject(Object value);
  }

  /**
   * This class is has a conflicting {@link JsonIgnore @JsonIgnore} value with {@link
   * GetterWithJsonIgnore}.
   */
  public interface GetterWithInconsistentJsonIgnoreValue extends PipelineOptions {
    @JsonIgnore(value = false)
    Object getObject();

    void setObject(Object value);
  }

  @Test
  public void testNotAllGettersAnnotatedWithJsonIgnore() throws Exception {
    // Initial construction is valid.
    GetterWithJsonIgnore options = PipelineOptionsFactory.as(GetterWithJsonIgnore.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected getter for property [object] to be marked with @JsonIgnore on all ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$GetterWithJsonIgnore, "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MissingSetter], "
            + "found only on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$GetterWithJsonIgnore]");

    // When we attempt to convert, we should error at this moment.
    options.as(CombinedObject.class);
  }

  /** Test interface. */
  public interface MultiGetters extends PipelineOptions {
    Object getObject();

    void setObject(Object value);

    @JsonIgnore
    Integer getOther();

    void setOther(Integer value);

    Void getConsistent();

    void setConsistent(Void consistent);
  }

  /** Test interface. */
  public interface MultipleGettersWithInconsistentJsonIgnore extends PipelineOptions {
    @JsonIgnore
    Object getObject();

    void setObject(Object value);

    Integer getOther();

    void setOther(Integer value);

    Void getConsistent();

    void setConsistent(Void consistent);
  }

  @Test
  public void testMultipleGettersWithInconsistentJsonIgnore() {
    // Initial construction is valid.
    MultiGetters options = PipelineOptionsFactory.as(MultiGetters.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Property getters are inconsistently marked with @JsonIgnore:");
    expectedException.expectMessage("property [object] to be marked on all");
    expectedException.expectMessage(
        "found only on [org.apache.beam.sdk.options." + "PipelineOptionsFactoryTest$MultiGetters]");
    expectedException.expectMessage("property [other] to be marked on all");
    expectedException.expectMessage(
        "found only on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentJsonIgnore]");

    expectedException.expectMessage(
        anyOf(
            containsString(
                java.util.Arrays.toString(
                    new String[] {
                      "org.apache.beam.sdk.options."
                          + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentJsonIgnore",
                      "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiGetters"
                    })),
            containsString(
                java.util.Arrays.toString(
                    new String[] {
                      "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiGetters",
                      "org.apache.beam.sdk.options."
                          + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentJsonIgnore"
                    }))));
    expectedException.expectMessage(not(containsString("property [consistent]")));

    // When we attempt to convert, we should error immediately
    options.as(MultipleGettersWithInconsistentJsonIgnore.class);
  }

  /** Test interface that has {@link Default @Default} on a setter for a property. */
  public interface SetterWithDefault extends PipelineOptions {
    String getValue();

    @Default.String("abc")
    void setValue(String value);
  }

  @Test
  public void testSetterAnnotatedWithDefault() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected setter for property [value] to not be marked with @Default on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$SetterWithDefault]");
    PipelineOptionsFactory.as(SetterWithDefault.class);
  }

  /** Test interface that has {@link Default @Default} on multiple setters. */
  public interface MultiSetterWithDefault extends SetterWithDefault {
    Integer getOther();

    @Default.String("abc")
    void setOther(Integer other);
  }

  @Test
  public void testMultipleSettersAnnotatedWithDefault() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Found setters marked with @Default:");
    expectedException.expectMessage(
        "property [other] should not be marked with @Default on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiSetterWithDefault]");
    expectedException.expectMessage(
        "property [value] should not be marked with @Default on ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$SetterWithDefault]");
    PipelineOptionsFactory.as(MultiSetterWithDefault.class);
  }

  /**
   * This class is has a conflicting field with {@link CombinedObject} that doesn't have {@link
   * Default @Default}.
   */
  public interface GetterWithDefault extends PipelineOptions {
    @Default.Integer(1)
    Object getObject();

    void setObject(Object value);
  }

  /**
   * This class is consistent with {@link GetterWithDefault} that has the same {@link
   * Default @Default}.
   */
  public interface GetterWithConsistentDefault extends PipelineOptions {
    @Default.Integer(1)
    Object getObject();

    void setObject(Object value);
  }

  /**
   * This class is inconsistent with {@link GetterWithDefault} that has a different {@link
   * Default @Default}.
   */
  public interface GetterWithInconsistentDefaultType extends PipelineOptions {
    @Default.String("abc")
    Object getObject();

    void setObject(Object value);
  }

  /**
   * This class is inconsistent with {@link GetterWithDefault} that has a different {@link
   * Default @Default} value.
   */
  public interface GetterWithInconsistentDefaultValue extends PipelineOptions {
    @Default.Integer(0)
    Object getObject();

    void setObject(Object value);
  }

  @Test
  public void testNotAllGettersAnnotatedWithDefault() throws Exception {
    // Initial construction is valid.
    GetterWithDefault options = PipelineOptionsFactory.as(GetterWithDefault.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Expected getter for property [object] to be marked with @Default on all ["
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$GetterWithDefault, "
            + "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MissingSetter], "
            + "found only on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$GetterWithDefault]");

    // When we attempt to convert, we should error at this moment.
    options.as(CombinedObject.class);
  }

  @Test
  public void testGettersAnnotatedWithConsistentDefault() throws Exception {
    GetterWithConsistentDefault options =
        PipelineOptionsFactory.as(GetterWithDefault.class).as(GetterWithConsistentDefault.class);

    assertEquals(1, options.getObject());
  }

  @Test
  public void testGettersAnnotatedWithInconsistentDefault() throws Exception {
    // Initial construction is valid.
    GetterWithDefault options = PipelineOptionsFactory.as(GetterWithDefault.class);

    expectedException.expect(IllegalArgumentException.class);

    // Make sure the error message says what the problem is, generally
    expectedException.expectMessage("contradictory annotations");

    // Make sure the error message gives actionable details about what
    // annotations were contradictory.
    // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8,
    // and "value=" element name prefix is absent in Java 21+
    expectedException.expectMessage(
        anyOf(
            containsString("Default.String(value=\"abc\")"),
            containsString("Default.String(value=abc)"),
            containsString("Default.String(\"abc\")"),
            containsString("Default.String(abc)")));
    expectedException.expectMessage(
        anyOf(containsString("Default.Integer(value=1"), containsString("Default.Integer(1")));

    // When we attempt to convert, we should error at this moment.
    options.as(GetterWithInconsistentDefaultType.class);
  }

  @Test
  public void testGettersAnnotatedWithInconsistentDefaultValue() throws Exception {
    // Initial construction is valid.
    GetterWithDefault options = PipelineOptionsFactory.as(GetterWithDefault.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        anyOf(
            containsString(
                "Property [object] is marked with contradictory annotations. Found ["
                    + "[Default.Integer(value=1) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithDefault#getObject()], "
                    + "[Default.Integer(value=0) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithInconsistentDefaultValue#getObject()]]."),
            containsString(
                "Property [object] is marked with contradictory annotations. Found ["
                    + "[Default.Integer(1) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithDefault#getObject()], "
                    + "[Default.Integer(0) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithInconsistentDefaultValue#getObject()]].")));

    // When we attempt to convert, we should error at this moment.
    options.as(GetterWithInconsistentDefaultValue.class);
  }

  @Test
  public void testGettersAnnotatedWithInconsistentJsonIgnoreValue() throws Exception {
    // Initial construction is valid.
    GetterWithJsonIgnore options = PipelineOptionsFactory.as(GetterWithJsonIgnore.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        anyOf(
            containsString(
                "Property [object] is marked with contradictory annotations. Found ["
                    + "[JsonIgnore(value=false) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithInconsistentJsonIgnoreValue#getObject()], "
                    + "[JsonIgnore(value=true) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithJsonIgnore#getObject()]]."),
            containsString(
                "Property [object] is marked with contradictory annotations. Found ["
                    + "[JsonIgnore(false) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithInconsistentJsonIgnoreValue#getObject()], "
                    + "[JsonIgnore(true) on org.apache.beam.sdk.options.PipelineOptionsFactoryTest"
                    + "$GetterWithJsonIgnore#getObject()]].")));

    // When we attempt to convert, we should error at this moment.
    options.as(GetterWithInconsistentJsonIgnoreValue.class);
  }

  /** Test interface. */
  public interface GettersWithMultipleDefault extends PipelineOptions {
    @Default.String("abc")
    @Default.Integer(0)
    Object getObject();

    void setObject(Object value);
  }

  @Test
  public void testGettersWithMultipleDefaults() throws Exception {
    expectedException.expect(IllegalArgumentException.class);

    // Make sure the error message says what the problem is, generally
    expectedException.expectMessage("contradictory annotations");

    // Make sure the error message gives actionable details about what annotations were
    // contradictory.
    // Note that the quotes in the unparsed string are present in Java 11 but absent in Java 8,
    // and "value=" element name prefix is absent in Java 21+
    expectedException.expectMessage(
        anyOf(
            containsString("Default.String(value=\"abc\")"),
            containsString("Default.String(value=abc)"),
            containsString("Default.String(\"abc\")"),
            containsString("Default.String(abc)")));
    expectedException.expectMessage(
        anyOf(containsString("Default.Integer(value=0)"), containsString("Default.Integer(0)")));

    // When we attempt to create, we should error at this moment.
    PipelineOptionsFactory.as(GettersWithMultipleDefault.class);
  }

  /** Test interface. */
  public interface MultiGettersWithDefault extends PipelineOptions {
    Object getObject();

    void setObject(Object value);

    @Default.Integer(1)
    Integer getOther();

    void setOther(Integer value);

    Void getConsistent();

    void setConsistent(Void consistent);
  }

  /** Test interface. */
  public interface MultipleGettersWithInconsistentDefault extends PipelineOptions {
    @Default.Boolean(true)
    Object getObject();

    void setObject(Object value);

    Integer getOther();

    void setOther(Integer value);

    Void getConsistent();

    void setConsistent(Void consistent);
  }

  @Test
  public void testMultipleGettersWithInconsistentDefault() {
    // Initial construction is valid.
    MultiGettersWithDefault options = PipelineOptionsFactory.as(MultiGettersWithDefault.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Property getters are inconsistently marked with @Default:");
    expectedException.expectMessage("property [object] to be marked on all");
    expectedException.expectMessage(
        "found only on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MultiGettersWithDefault]");
    expectedException.expectMessage("property [other] to be marked on all");
    expectedException.expectMessage(
        "found only on [org.apache.beam.sdk.options."
            + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentDefault]");

    expectedException.expectMessage(
        anyOf(
            containsString(
                java.util.Arrays.toString(
                    new String[] {
                      "org.apache.beam.sdk.options."
                          + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentDefault",
                      "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiGettersWithDefault"
                    })),
            containsString(
                java.util.Arrays.toString(
                    new String[] {
                      "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$MultiGettersWithDefault",
                      "org.apache.beam.sdk.options."
                          + "PipelineOptionsFactoryTest$MultipleGettersWithInconsistentDefault"
                    }))));
    expectedException.expectMessage(not(containsString("property [consistent]")));

    // When we attempt to convert, we should error immediately
    options.as(MultipleGettersWithInconsistentDefault.class);
  }

  @Test
  public void testAppNameIsNotOverriddenWhenPassedInViaCommandLine() {
    ApplicationNameOptions options =
        PipelineOptionsFactory.fromArgs("--appName=testAppName").as(ApplicationNameOptions.class);
    assertEquals("testAppName", options.getAppName());
  }

  @Test
  public void testPropertyIsSetOnRegisteredPipelineOptionNotPartOfOriginalInterface() {
    PipelineOptions options = PipelineOptionsFactory.fromArgs("--streaming").create();
    assertTrue(options.as(StreamingOptions.class).isStreaming());
  }

  /** A test interface containing all the primitives. */
  public interface Primitives extends PipelineOptions {
    boolean getBoolean();

    void setBoolean(boolean value);

    char getChar();

    void setChar(char value);

    byte getByte();

    void setByte(byte value);

    short getShort();

    void setShort(short value);

    int getInt();

    void setInt(int value);

    long getLong();

    void setLong(long value);

    float getFloat();

    void setFloat(float value);

    double getDouble();

    void setDouble(double value);
  }

  @Test
  public void testPrimitives() {
    String[] args =
        new String[] {
          "--boolean=true",
          "--char=d",
          "--byte=12",
          "--short=300",
          "--int=100000",
          "--long=123890123890",
          "--float=55.5",
          "--double=12.3"
        };

    Primitives options = PipelineOptionsFactory.fromArgs(args).as(Primitives.class);
    assertTrue(options.getBoolean());
    assertEquals('d', options.getChar());
    assertEquals((byte) 12, options.getByte());
    assertEquals((short) 300, options.getShort());
    assertEquals(100000, options.getInt());
    assertEquals(123890123890L, options.getLong());
    assertEquals(55.5f, options.getFloat(), 0.0f);
    assertEquals(12.3, options.getDouble(), 0.0);
  }

  @Test
  public void testBooleanShorthandArgument() {
    String[] args = new String[] {"--boolean"};

    Primitives options = PipelineOptionsFactory.fromArgs(args).as(Primitives.class);
    assertTrue(options.getBoolean());
  }

  @Test
  public void testEmptyValueNotAllowed() {
    String[] args = new String[] {"--byte="};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(args).as(Primitives.class);
  }

  /** Enum used for testing PipelineOptions CLI parsing. */
  public enum TestEnum {
    Value,
    Value2
  }

  /** A test interface containing all supported objects. */
  public interface Objects extends PipelineOptions {
    Boolean getBoolean();

    void setBoolean(Boolean value);

    Character getChar();

    void setChar(Character value);

    Byte getByte();

    void setByte(Byte value);

    Short getShort();

    void setShort(Short value);

    Integer getInt();

    void setInt(Integer value);

    Long getLong();

    void setLong(Long value);

    Float getFloat();

    void setFloat(Float value);

    Double getDouble();

    void setDouble(Double value);

    String getString();

    void setString(String value);

    String getEmptyString();

    void setEmptyString(String value);

    Class<?> getClassValue();

    void setClassValue(Class<?> value);

    TestEnum getEnum();

    void setEnum(TestEnum value);

    ValueProvider<String> getStringValue();

    void setStringValue(ValueProvider<String> value);

    ValueProvider<Long> getLongValue();

    void setLongValue(ValueProvider<Long> value);

    ValueProvider<TestEnum> getEnumValue();

    void setEnumValue(ValueProvider<TestEnum> value);
  }

  @Test
  public void testObjects() {
    String[] args =
        new String[] {
          "--boolean=true",
          "--char=d",
          "--byte=12",
          "--short=300",
          "--int=100000",
          "--long=123890123890",
          "--float=55.5",
          "--double=12.3",
          "--string=stringValue",
          "--emptyString=",
          "--classValue=" + PipelineOptionsFactoryTest.class.getName(),
          "--enum=" + TestEnum.Value,
          "--stringValue=beam",
          "--longValue=12389049585840",
          "--enumValue=" + TestEnum.Value
        };

    Objects options = PipelineOptionsFactory.fromArgs(args).as(Objects.class);
    assertTrue(options.getBoolean());
    assertEquals(Character.valueOf('d'), options.getChar());
    assertEquals(Byte.valueOf((byte) 12), options.getByte());
    assertEquals(Short.valueOf((short) 300), options.getShort());
    assertEquals(Integer.valueOf(100000), options.getInt());
    assertEquals(Long.valueOf(123890123890L), options.getLong());
    assertEquals(55.5f, options.getFloat(), 0.0f);
    assertEquals(12.3, options.getDouble(), 0.0);
    assertEquals("stringValue", options.getString());
    assertTrue(options.getEmptyString().isEmpty());
    assertEquals(PipelineOptionsFactoryTest.class, options.getClassValue());
    assertEquals(TestEnum.Value, options.getEnum());
    assertEquals("beam", options.getStringValue().get());
    assertEquals(Long.valueOf(12389049585840L), options.getLongValue().get());
    assertEquals(TestEnum.Value, options.getEnumValue().get());
  }

  @Test
  public void testStringValueProvider() {
    String[] args = new String[] {"--stringValue=beam"};
    String[] emptyArgs = new String[] {"--stringValue="};
    Objects options = PipelineOptionsFactory.fromArgs(args).as(Objects.class);
    assertEquals("beam", options.getStringValue().get());
    options = PipelineOptionsFactory.fromArgs(emptyArgs).as(Objects.class);
    assertEquals("", options.getStringValue().get());
  }

  @Test
  public void testLongValueProvider() {
    String[] args = new String[] {"--longValue=12345678762"};
    String[] emptyArgs = new String[] {"--longValue="};
    Objects options = PipelineOptionsFactory.fromArgs(args).as(Objects.class);
    assertEquals(Long.valueOf(12345678762L), options.getLongValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Objects.class);
  }

  @Test
  public void testEnumValueProvider() {
    String[] args = new String[] {"--enumValue=" + TestEnum.Value};
    String[] emptyArgs = new String[] {"--enumValue="};
    Objects options = PipelineOptionsFactory.fromArgs(args).as(Objects.class);
    assertEquals(TestEnum.Value, options.getEnumValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Objects.class);
  }

  /** A test class for verifying JSON -> Object conversion. */
  public static class ComplexType {
    String value;
    String value2;

    public ComplexType(@JsonProperty("key") String value, @JsonProperty("key2") String value2) {
      this.value = value;
      this.value2 = value2;
    }
  }

  /** A test interface for verifying JSON -> complex type conversion. */
  public interface ComplexTypes extends PipelineOptions {
    Map<String, String> getMap();

    void setMap(Map<String, String> value);

    ComplexType getObject();

    void setObject(ComplexType value);

    ValueProvider<ComplexType> getObjectValue();

    void setObjectValue(ValueProvider<ComplexType> value);
  }

  @Test
  public void testComplexTypes() {
    String[] args =
        new String[] {
          "--map={\"key\":\"value\",\"key2\":\"value2\"}",
          "--object={\"key\":\"value\",\"key2\":\"value2\"}",
          "--objectValue={\"key\":\"value\",\"key2\":\"value2\"}"
        };
    ComplexTypes options = PipelineOptionsFactory.fromArgs(args).as(ComplexTypes.class);
    assertEquals(ImmutableMap.of("key", "value", "key2", "value2"), options.getMap());
    assertEquals("value", options.getObject().value);
    assertEquals("value2", options.getObject().value2);
    assertEquals("value", options.getObjectValue().get().value);
    assertEquals("value2", options.getObjectValue().get().value2);
  }

  @JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
  @JsonSubTypes({
    @JsonSubTypes.Type(value = PolymorphicTypeOne.class, name = "one"),
    @JsonSubTypes.Type(value = PolymorphicTypeTwo.class, name = "two")
  })
  public abstract static class PolymorphicType {
    String key;

    @JsonProperty("key")
    public String getKey() {
      return key;
    }

    public void setKey(String key) {
      this.key = key;
    }
  }

  public static class PolymorphicTypeOne extends PolymorphicType {}

  public static class PolymorphicTypeTwo extends PolymorphicType {}

  public interface PolymorphicTypes extends PipelineOptions {
    PolymorphicType getObject();

    void setObject(PolymorphicType value);

    ValueProvider<PolymorphicType> getObjectValue();

    void setObjectValue(ValueProvider<PolymorphicType> value);
  }

  @Test
  public void testPolymorphicType() {
    String[] args =
        new String[] {
          "--object={\"key\":\"value\",\"@type\":\"one\"}",
          "--objectValue={\"key\":\"value\",\"@type\":\"two\"}"
        };
    PolymorphicTypes options = PipelineOptionsFactory.fromArgs(args).as(PolymorphicTypes.class);
    assertEquals("value", options.getObject().key);
    assertEquals(PolymorphicTypeOne.class, options.getObject().getClass());

    assertEquals("value", options.getObjectValue().get().key);
    assertEquals(PolymorphicTypeTwo.class, options.getObjectValue().get().getClass());
  }

  @Test
  public void testMissingArgument() {
    String[] args = new String[] {};

    Objects options = PipelineOptionsFactory.fromArgs(args).as(Objects.class);
    assertNull(options.getString());
  }

  /** A test interface containing all supported array return types. */
  public interface Arrays extends PipelineOptions {
    boolean[] getBoolean();

    void setBoolean(boolean[] value);

    char[] getChar();

    void setChar(char[] value);

    short[] getShort();

    void setShort(short[] value);

    int[] getInt();

    void setInt(int[] value);

    long[] getLong();

    void setLong(long[] value);

    float[] getFloat();

    void setFloat(float[] value);

    double[] getDouble();

    void setDouble(double[] value);

    String[] getString();

    void setString(String[] value);

    Class<?>[] getClassValue();

    void setClassValue(Class<?>[] value);

    TestEnum[] getEnum();

    void setEnum(TestEnum[] value);

    ValueProvider<String[]> getStringValue();

    void setStringValue(ValueProvider<String[]> value);

    ValueProvider<Long[]> getLongValue();

    void setLongValue(ValueProvider<Long[]> value);

    ValueProvider<TestEnum[]> getEnumValue();

    void setEnumValue(ValueProvider<TestEnum[]> value);
  }

  @Test
  @SuppressWarnings("rawtypes")
  public void testArrays() {
    String[] args =
        new String[] {
          "--boolean=true",
          "--boolean=true",
          "--boolean=false",
          "--char=d",
          "--char=e",
          "--char=f",
          "--short=300",
          "--short=301",
          "--short=302",
          "--int=100000",
          "--int=100001",
          "--int=100002",
          "--long=123890123890",
          "--long=123890123891",
          "--long=123890123892",
          "--float=55.5",
          "--float=55.6",
          "--float=55.7",
          "--double=12.3",
          "--double=12.4",
          "--double=12.5",
          "--string=stringValue1",
          "--string=stringValue2",
          "--string=stringValue3",
          "--classValue=" + PipelineOptionsFactory.class.getName(),
          "--classValue=" + PipelineOptionsFactoryTest.class.getName(),
          "--enum=" + TestEnum.Value,
          "--enum=" + TestEnum.Value2,
          "--stringValue=abc",
          "--stringValue=beam",
          "--longValue=123890123890",
          "--longValue=123890123891",
          "--enumValue=" + TestEnum.Value,
          "--enumValue=" + TestEnum.Value2
        };

    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    boolean[] bools = options.getBoolean();
    assertTrue(bools[0] && bools[1] && !bools[2]);
    assertArrayEquals(new char[] {'d', 'e', 'f'}, options.getChar());
    assertArrayEquals(new short[] {300, 301, 302}, options.getShort());
    assertArrayEquals(new int[] {100000, 100001, 100002}, options.getInt());
    assertArrayEquals(new long[] {123890123890L, 123890123891L, 123890123892L}, options.getLong());
    assertArrayEquals(new float[] {55.5f, 55.6f, 55.7f}, options.getFloat(), 0.0f);
    assertArrayEquals(new double[] {12.3, 12.4, 12.5}, options.getDouble(), 0.0);
    assertArrayEquals(
        new String[] {"stringValue1", "stringValue2", "stringValue3"}, options.getString());
    assertArrayEquals(
        new Class[] {PipelineOptionsFactory.class, PipelineOptionsFactoryTest.class},
        options.getClassValue());
    assertArrayEquals(new TestEnum[] {TestEnum.Value, TestEnum.Value2}, options.getEnum());
    assertArrayEquals(new String[] {"abc", "beam"}, options.getStringValue().get());
    assertArrayEquals(new Long[] {123890123890L, 123890123891L}, options.getLongValue().get());
    assertArrayEquals(
        new TestEnum[] {TestEnum.Value, TestEnum.Value2}, options.getEnumValue().get());
  }

  @Test
  @SuppressWarnings("rawtypes")
  public void testEmptyInStringArrays() {
    String[] args = new String[] {"--string=", "--string=", "--string="};

    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(new String[] {"", "", ""}, options.getString());
  }

  @Test
  @SuppressWarnings("rawtypes")
  public void testEmptyInStringArraysWithCommaList() {
    String[] args = new String[] {"--string=a,,b"};

    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(new String[] {"a", "", "b"}, options.getString());
  }

  @Test
  public void testEmptyInNonStringArrays() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());

    String[] args = new String[] {"--boolean=true", "--boolean=", "--boolean=false"};

    PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
  }

  @Test
  public void testEmptyInNonStringArraysWithCommaList() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());

    String[] args = new String[] {"--int=1,,9"};
    PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
  }

  @Test
  public void testStringArrayValueProvider() {
    String[] args = new String[] {"--stringValue=abc", "--stringValue=xyz"};
    String[] commaArgs = new String[] {"--stringValue=abc,xyz"};
    String[] emptyArgs = new String[] {"--stringValue=", "--stringValue="};
    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(new String[] {"abc", "xyz"}, options.getStringValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Arrays.class);
    assertArrayEquals(new String[] {"abc", "xyz"}, options.getStringValue().get());
    options = PipelineOptionsFactory.fromArgs(emptyArgs).as(Arrays.class);
    assertArrayEquals(new String[] {"", ""}, options.getStringValue().get());
  }

  @Test
  public void testLongArrayValueProvider() {
    String[] args = new String[] {"--longValue=12345678762", "--longValue=12345678763"};
    String[] commaArgs = new String[] {"--longValue=12345678762,12345678763"};
    String[] emptyArgs = new String[] {"--longValue=", "--longValue="};
    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(new Long[] {12345678762L, 12345678763L}, options.getLongValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Arrays.class);
    assertArrayEquals(new Long[] {12345678762L, 12345678763L}, options.getLongValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Arrays.class);
  }

  @Test
  public void testEnumArrayValueProvider() {
    String[] args =
        new String[] {"--enumValue=" + TestEnum.Value, "--enumValue=" + TestEnum.Value2};
    String[] commaArgs = new String[] {"--enumValue=" + TestEnum.Value + "," + TestEnum.Value2};
    String[] emptyArgs = new String[] {"--enumValue="};
    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(
        new TestEnum[] {TestEnum.Value, TestEnum.Value2}, options.getEnumValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Arrays.class);
    assertArrayEquals(
        new TestEnum[] {TestEnum.Value, TestEnum.Value2}, options.getEnumValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Arrays.class);
  }

  @Test
  public void testOutOfOrderArrays() {
    String[] args =
        new String[] {
          "--char=d", "--boolean=true", "--boolean=true", "--char=e", "--char=f", "--boolean=false"
        };

    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    boolean[] bools = options.getBoolean();
    assertTrue(bools[0] && bools[1] && !bools[2]);
    assertArrayEquals(new char[] {'d', 'e', 'f'}, options.getChar());
  }

  /** A test interface containing all supported List return types. */
  public interface Lists extends PipelineOptions {
    List<String> getString();

    void setString(List<String> value);

    List<Integer> getInteger();

    void setInteger(List<Integer> value);

    @SuppressWarnings("rawtypes")
    List getList();

    @SuppressWarnings("rawtypes")
    void setList(List value);

    ValueProvider<List<String>> getStringValue();

    void setStringValue(ValueProvider<List<String>> value);

    ValueProvider<List<Long>> getLongValue();

    void setLongValue(ValueProvider<List<Long>> value);

    ValueProvider<List<TestEnum>> getEnumValue();

    void setEnumValue(ValueProvider<List<TestEnum>> value);
  }

  @Test
  public void testListRawDefaultsToString() {
    String[] manyArgs =
        new String[] {"--list=stringValue1", "--list=stringValue2", "--list=stringValue3"};

    String[] manyArgsWithEmptyString =
        new String[] {"--list=stringValue1", "--list=", "--list=stringValue3"};

    Lists options = PipelineOptionsFactory.fromArgs(manyArgs).as(Lists.class);
    assertEquals(
        ImmutableList.of("stringValue1", "stringValue2", "stringValue3"), options.getList());
    options = PipelineOptionsFactory.fromArgs(manyArgsWithEmptyString).as(Lists.class);
    assertEquals(ImmutableList.of("stringValue1", "", "stringValue3"), options.getList());
  }

  @Test
  public void testListString() {
    String[] manyArgs =
        new String[] {"--string=stringValue1", "--string=stringValue2", "--string=stringValue3"};
    String[] oneArg = new String[] {"--string=stringValue1"};
    String[] emptyArg = new String[] {"--string="};

    Lists options = PipelineOptionsFactory.fromArgs(manyArgs).as(Lists.class);
    assertEquals(
        ImmutableList.of("stringValue1", "stringValue2", "stringValue3"), options.getString());

    options = PipelineOptionsFactory.fromArgs(oneArg).as(Lists.class);
    assertEquals(ImmutableList.of("stringValue1"), options.getString());

    options = PipelineOptionsFactory.fromArgs(emptyArg).as(Lists.class);
    assertEquals(ImmutableList.of(""), options.getString());
  }

  @Test
  public void testListInt() {
    String[] manyArgs = new String[] {"--integer=1", "--integer=2", "--integer=3"};
    String[] manyArgsShort = new String[] {"--integer=1,2,3"};
    String[] oneArg = new String[] {"--integer=1"};
    String[] missingArg = new String[] {"--integer="};

    Lists options = PipelineOptionsFactory.fromArgs(manyArgs).as(Lists.class);
    assertEquals(ImmutableList.of(1, 2, 3), options.getInteger());
    options = PipelineOptionsFactory.fromArgs(manyArgsShort).as(Lists.class);
    assertEquals(ImmutableList.of(1, 2, 3), options.getInteger());
    options = PipelineOptionsFactory.fromArgs(oneArg).as(Lists.class);
    assertEquals(ImmutableList.of(1), options.getInteger());

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage("java.util.List<java.lang.Integer>"));
    PipelineOptionsFactory.fromArgs(missingArg).as(Lists.class);
  }

  @Test
  public void testListShorthand() {
    String[] args = new String[] {"--string=stringValue1,stringValue2,stringValue3"};

    Lists options = PipelineOptionsFactory.fromArgs(args).as(Lists.class);
    assertEquals(
        ImmutableList.of("stringValue1", "stringValue2", "stringValue3"), options.getString());
  }

  @Test
  public void testMixedShorthandAndLongStyleList() {
    String[] args =
        new String[] {
          "--char=d",
          "--char=e",
          "--char=f",
          "--char=g,h,i",
          "--char=j",
          "--char=k",
          "--char=l",
          "--char=m,n,o"
        };

    Arrays options = PipelineOptionsFactory.fromArgs(args).as(Arrays.class);
    assertArrayEquals(
        new char[] {'d', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o'}, options.getChar());
  }

  @Test
  public void testStringListValueProvider() {
    String[] args = new String[] {"--stringValue=abc", "--stringValue=xyz"};
    String[] commaArgs = new String[] {"--stringValue=abc,xyz"};
    String[] emptyArgs = new String[] {"--stringValue=", "--stringValue="};
    Lists options = PipelineOptionsFactory.fromArgs(args).as(Lists.class);
    assertEquals(ImmutableList.of("abc", "xyz"), options.getStringValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Lists.class);
    assertEquals(ImmutableList.of("abc", "xyz"), options.getStringValue().get());
    options = PipelineOptionsFactory.fromArgs(emptyArgs).as(Lists.class);
    assertEquals(ImmutableList.of("", ""), options.getStringValue().get());
  }

  @Test
  public void testLongListValueProvider() {
    String[] args = new String[] {"--longValue=12345678762", "--longValue=12345678763"};
    String[] commaArgs = new String[] {"--longValue=12345678762,12345678763"};
    String[] emptyArgs = new String[] {"--longValue=", "--longValue="};
    Lists options = PipelineOptionsFactory.fromArgs(args).as(Lists.class);
    assertEquals(ImmutableList.of(12345678762L, 12345678763L), options.getLongValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Lists.class);
    assertEquals(ImmutableList.of(12345678762L, 12345678763L), options.getLongValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Lists.class);
  }

  @Test
  public void testEnumListValueProvider() {
    String[] args =
        new String[] {"--enumValue=" + TestEnum.Value, "--enumValue=" + TestEnum.Value2};
    String[] commaArgs = new String[] {"--enumValue=" + TestEnum.Value + "," + TestEnum.Value2};
    String[] emptyArgs = new String[] {"--enumValue="};
    Lists options = PipelineOptionsFactory.fromArgs(args).as(Lists.class);
    assertEquals(ImmutableList.of(TestEnum.Value, TestEnum.Value2), options.getEnumValue().get());
    options = PipelineOptionsFactory.fromArgs(commaArgs).as(Lists.class);
    assertEquals(ImmutableList.of(TestEnum.Value, TestEnum.Value2), options.getEnumValue().get());
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(emptyStringErrorMessage());
    PipelineOptionsFactory.fromArgs(emptyArgs).as(Lists.class);
  }

  @Test
  public void testSetASingularAttributeUsingAListThrowsAnError() {
    String[] args = new String[] {"--string=100", "--string=200"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("expected one element but was");
    PipelineOptionsFactory.fromArgs(args).as(Objects.class);
  }

  @Test
  public void testSetASingularAttributeUsingAListIsIgnoredWithoutStrictParsing() {
    String[] args = new String[] {"--diskSizeGb=100", "--diskSizeGb=200"};
    PipelineOptionsFactory.fromArgs(args).withoutStrictParsing().create();
    expectedLogs.verifyWarn("Strict parsing is disabled, ignoring option");
  }

  private interface NonPublicPipelineOptions extends PipelineOptions {}

  @Test
  public void testNonPublicInterfaceThrowsException() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Please mark non-public interface "
            + NonPublicPipelineOptions.class.getName()
            + " as public.");

    PipelineOptionsFactory.as(NonPublicPipelineOptions.class);
  }

  /** A test interface containing all supported List return types. */
  public interface Maps extends PipelineOptions {
    Map<Integer, Integer> getMap();

    void setMap(Map<Integer, Integer> value);

    Map<Integer, Map<Integer, Integer>> getNestedMap();

    void setNestedMap(Map<Integer, Map<Integer, Integer>> value);
  }

  @Test
  public void testMapIntInt() {
    String[] manyArgsShort = new String[] {"--map={\"1\":1,\"2\":2}"};
    String[] oneArg = new String[] {"--map={\"1\":1}"};
    String[] missingArg = new String[] {"--map="};

    Maps options = PipelineOptionsFactory.fromArgs(manyArgsShort).as(Maps.class);
    assertEquals(ImmutableMap.of(1, 1, 2, 2), options.getMap());
    options = PipelineOptionsFactory.fromArgs(oneArg).as(Maps.class);
    assertEquals(ImmutableMap.of(1, 1), options.getMap());

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        emptyStringErrorMessage("java.util.Map<java.lang.Integer, java.lang.Integer>"));
    PipelineOptionsFactory.fromArgs(missingArg).as(Maps.class);
  }

  @Test
  public void testNestedMap() {
    String[] manyArgsShort = new String[] {"--nestedMap={\"1\":{\"1\":1},\"2\":{\"2\":2}}"};
    String[] oneArg = new String[] {"--nestedMap={\"1\":{\"1\":1}}"};
    String[] missingArg = new String[] {"--nestedMap="};

    Maps options = PipelineOptionsFactory.fromArgs(manyArgsShort).as(Maps.class);
    assertEquals(
        ImmutableMap.of(
            1, ImmutableMap.of(1, 1),
            2, ImmutableMap.of(2, 2)),
        options.getNestedMap());
    options = PipelineOptionsFactory.fromArgs(oneArg).as(Maps.class);
    assertEquals(ImmutableMap.of(1, ImmutableMap.of(1, 1)), options.getNestedMap());

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        emptyStringErrorMessage(
            "java.util.Map<java.lang.Integer, java.util.Map<java.lang.Integer,"
                + " java.lang.Integer>>"));
    PipelineOptionsFactory.fromArgs(missingArg).as(Maps.class);
  }

  @Test
  public void testSettingRunner() {
    String[] args = new String[] {"--runner=" + RegisteredTestRunner.class.getSimpleName()};

    PipelineOptions options = PipelineOptionsFactory.fromArgs(args).create();
    assertEquals(RegisteredTestRunner.class, options.getRunner());
  }

  @Test
  public void testSettingRunnerFullName() {
    String[] args = new String[] {String.format("--runner=%s", CrashingRunner.class.getName())};
    PipelineOptions opts = PipelineOptionsFactory.fromArgs(args).create();
    assertEquals(opts.getRunner(), CrashingRunner.class);
  }

  @Test
  public void testSettingUnknownRunner() {
    String[] args = new String[] {"--runner=UnknownRunner"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Unknown 'runner' specified 'UnknownRunner', supported " + "pipeline runners");
    Set<String> registeredRunners = PipelineOptionsFactory.getRegisteredRunners().keySet();
    assertThat(registeredRunners, hasItem(REGISTERED_RUNNER.getSimpleName().toLowerCase()));

    expectedException.expectMessage(
        PipelineOptionsFactory.CACHE.get().getSupportedRunners().toString());

    PipelineOptionsFactory.fromArgs(args).create();
  }

  private static class ExampleTestRunner extends PipelineRunner<PipelineResult> {
    @Override
    public PipelineResult run(Pipeline pipeline) {
      return null;
    }
  }

  @Test
  public void testSettingRunnerCanonicalClassNameNotInSupportedExists() {
    String[] args = new String[] {String.format("--runner=%s", ExampleTestRunner.class.getName())};
    PipelineOptions opts = PipelineOptionsFactory.fromArgs(args).create();
    assertEquals(opts.getRunner(), ExampleTestRunner.class);
  }

  @Test
  public void testSettingRunnerCanonicalClassNameNotInSupportedNotPipelineRunner() {
    String[] args = new String[] {"--runner=java.lang.String"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("does not implement PipelineRunner");
    expectedException.expectMessage("java.lang.String");

    PipelineOptionsFactory.fromArgs(args).create();
  }

  @Test
  public void testUsingArgumentWithUnknownPropertyIsNotAllowed() {
    String[] args = new String[] {"--unknownProperty=value"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("missing a property named 'unknownProperty'");
    PipelineOptionsFactory.fromArgs(args).create();
  }

  /** Test interface. */
  public interface SuggestedOptions extends PipelineOptions {
    String getAbc();

    void setAbc(String value);

    String getAbcdefg();

    void setAbcdefg(String value);
  }

  @Test
  public void testUsingArgumentWithMisspelledPropertyGivesASuggestion() {
    String[] args = new String[] {"--ab=value"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("missing a property named 'ab'. Did you mean 'abc'?");
    PipelineOptionsFactory.fromArgs(args).as(SuggestedOptions.class);
  }

  @Test
  public void testUsingArgumentWithMisspelledPropertyGivesMultipleSuggestions() {
    String[] args = new String[] {"--abcde=value"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "missing a property named 'abcde'. Did you mean one of [abc, abcdefg]?");
    PipelineOptionsFactory.fromArgs(args).as(SuggestedOptions.class);
  }

  @Test
  public void testUsingArgumentWithUnknownPropertyIsIgnoredWithoutStrictParsing() {
    String[] args = new String[] {"--unknownProperty=value"};
    PipelineOptionsFactory.fromArgs(args).withoutStrictParsing().create();
    expectedLogs.verifyWarn("Strict parsing is disabled, ignoring option 'unknownProperty'");
  }

  @Test
  public void testUsingArgumentStartingWithIllegalCharacterIsNotAllowed() {
    String[] args = new String[] {" --diskSizeGb=100"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Argument ' --diskSizeGb=100' does not begin with '--'");
    PipelineOptionsFactory.fromArgs(args).create();
  }

  @Test
  public void testUsingArgumentStartingWithIllegalCharacterIsIgnoredWithoutStrictParsing() {
    String[] args = new String[] {" --diskSizeGb=100"};
    PipelineOptionsFactory.fromArgs(args).withoutStrictParsing().create();
    expectedLogs.verifyWarn("Strict parsing is disabled, ignoring option");
  }

  @Test
  public void testEmptyArgumentIsIgnored() {
    String[] args =
        new String[] {"", "--string=100", "", "", "--runner=" + REGISTERED_RUNNER.getSimpleName()};
    PipelineOptionsFactory.fromArgs(args).as(Objects.class);
  }

  @Test
  public void testNullArgumentIsIgnored() {
    String[] args =
        new String[] {"--string=100", null, null, "--runner=" + REGISTERED_RUNNER.getSimpleName()};
    PipelineOptionsFactory.fromArgs(args).as(Objects.class);
  }

  @Test
  public void testUsingArgumentWithInvalidNameIsNotAllowed() {
    String[] args = new String[] {"--=100"};
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Argument '--=100' starts with '--='");
    PipelineOptionsFactory.fromArgs(args).create();
  }

  @Test
  public void testUsingArgumentWithInvalidNameIsIgnoredWithoutStrictParsing() {
    String[] args = new String[] {"--=100"};
    PipelineOptionsFactory.fromArgs(args).withoutStrictParsing().create();
    expectedLogs.verifyWarn("Strict parsing is disabled, ignoring option");
  }

  @Test
  public void testWhenNoHelpIsRequested() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    assertFalse(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertEquals("", output);
  }

  @Test
  public void testDefaultHelpAsArgument() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "true");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("The set of registered options are:"));
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
    assertThat(output, containsString("Use --help=<OptionsName> for detailed help."));
  }

  @Test
  public void testSpecificHelpAsArgument() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "org.apache.beam.sdk.options.PipelineOptions");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
    assertThat(output, containsString("--runner"));
    assertThat(output, containsString("Default: " + DEFAULT_RUNNER_NAME));
    assertThat(
        output, containsString("The pipeline runner that will be used to execute the pipeline."));
  }

  @Test
  public void testSpecificHelpAsArgumentWithSimpleClassName() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "PipelineOptions");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
    assertThat(output, containsString("--runner"));
    assertThat(output, containsString("Default: " + DEFAULT_RUNNER_NAME));
    assertThat(
        output, containsString("The pipeline runner that will be used to execute the pipeline."));
  }

  @Test
  public void testSpecificHelpAsArgumentWithClassNameSuffix() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "options.PipelineOptions");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
    assertThat(output, containsString("--runner"));
    assertThat(output, containsString("Default: " + DEFAULT_RUNNER_NAME));
    assertThat(
        output, containsString("The pipeline runner that will be used to execute the pipeline."));
  }

  /** Used for a name collision test with the other NameConflict interfaces. */
  public static class NameConflictClassA {
    /** Used for a name collision test with the other NameConflict interfaces. */
    public interface NameConflict extends PipelineOptions {}
  }

  /** Used for a name collision test with the other NameConflict interfaces. */
  public static class NameConflictClassB {
    /** Used for a name collision test with the other NameConflict interfaces. */
    public interface NameConflict extends PipelineOptions {}
  }

  @Test
  public void testShortnameSpecificHelpHasMultipleMatches() {
    PipelineOptionsFactory.register(NameConflictClassA.NameConflict.class);
    PipelineOptionsFactory.register(NameConflictClassB.NameConflict.class);
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "NameConflict");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("Multiple matches found for NameConflict"));
    assertThat(
        output,
        containsString(
            "org.apache.beam.sdk.options."
                + "PipelineOptionsFactoryTest$NameConflictClassA$NameConflict"));
    assertThat(
        output,
        containsString(
            "org.apache.beam.sdk.options."
                + "PipelineOptionsFactoryTest$NameConflictClassB$NameConflict"));
    assertThat(output, containsString("The set of registered options are:"));
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
  }

  @Test
  public void testHelpWithOptionThatOutputsValidEnumTypes() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", Objects.class.getName());
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("<Value | Value2>"));
  }

  @Test
  public void testHelpWithBadOptionNameAsArgument() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "org.apache.beam.sdk.Pipeline");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("Unable to find option org.apache.beam.sdk.Pipeline"));
    assertThat(output, containsString("The set of registered options are:"));
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
  }

  @Test
  public void testHelpWithHiddenMethodAndInterface() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ListMultimap<String, String> arguments = ArrayListMultimap.create();
    arguments.put("help", "org.apache.beam.sdk.option.DataflowPipelineOptions");
    assertTrue(
        PipelineOptionsFactory.printHelpUsageAndExitIfNeeded(
            arguments, new PrintStream(baos), false /* exit */));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    // A hidden interface.
    assertThat(
        output, not(containsString("org.apache.beam.sdk.options.DataflowPipelineDebugOptions")));
    // A hidden option.
    assertThat(output, not(containsString("--gcpCredential")));
  }

  @Test
  public void testProgrammaticPrintHelp() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    PipelineOptionsFactory.printHelp(new PrintStream(baos));
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("The set of registered options are:"));
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
  }

  @Test
  public void testProgrammaticPrintHelpForSpecificType() {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    PipelineOptionsFactory.printHelp(new PrintStream(baos), PipelineOptions.class);
    String output = new String(baos.toByteArray(), StandardCharsets.UTF_8);
    assertThat(output, containsString("org.apache.beam.sdk.options.PipelineOptions"));
    assertThat(output, containsString("--runner"));
    assertThat(output, containsString("Default: " + DEFAULT_RUNNER_NAME));
    assertThat(
        output, containsString("The pipeline runner that will be used to execute the pipeline."));
  }

  /** Test interface. */
  public interface PipelineOptionsInheritedInvalid
      extends Invalid1, InvalidPipelineOptions2, PipelineOptions {
    String getFoo();

    void setFoo(String value);
  }

  /** Test interface. */
  public interface InvalidPipelineOptions1 {
    String getBar();

    void setBar(String value);
  }

  /** Test interface. */
  public interface Invalid1 extends InvalidPipelineOptions1 {
    @Override
    String getBar();

    @Override
    void setBar(String value);
  }

  /** Test interface. */
  public interface InvalidPipelineOptions2 {
    String getBar();

    void setBar(String value);
  }

  @Test
  public void testAllFromPipelineOptions() {
    // TODO: Java core test failing on windows, https://github.com/apache/beam/issues/20466
    assumeFalse(SystemUtils.IS_OS_WINDOWS);
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "All inherited interfaces of"
            + " [org.apache.beam.sdk.options.PipelineOptionsFactoryTest$PipelineOptionsInheritedInvalid]"
            + " should inherit from the PipelineOptions interface. The following inherited"
            + " interfaces do not:\n"
            + " - org.apache.beam.sdk.options.PipelineOptionsFactoryTest$InvalidPipelineOptions1\n"
            + " - org.apache.beam.sdk.options.PipelineOptionsFactoryTest$InvalidPipelineOptions2");

    PipelineOptionsFactory.as(PipelineOptionsInheritedInvalid.class);
  }

  private String emptyStringErrorMessage() {
    return emptyStringErrorMessage(null);
  }

  private String emptyStringErrorMessage(String type) {
    String msg =
        "Empty argument value is only allowed for String, String Array, "
            + "Collections of Strings or any of these types in a parameterized ValueProvider";
    if (type != null) {
      return String.format("%s, but received: %s", msg, type);
    } else {
      return msg;
    }
  }

  private static class RegisteredTestRunner extends PipelineRunner<PipelineResult> {
    public static PipelineRunner<PipelineResult> fromOptions(
        @SuppressWarnings("unused") PipelineOptions options) {
      return new RegisteredTestRunner();
    }

    @Override
    public PipelineResult run(Pipeline p) {
      throw new IllegalArgumentException();
    }
  }

  /**
   * A {@link PipelineRunnerRegistrar} to demonstrate default {@link PipelineRunner} registration.
   */
  @AutoService(PipelineRunnerRegistrar.class)
  public static class RegisteredTestRunnerRegistrar implements PipelineRunnerRegistrar {
    @Override
    public Iterable<Class<? extends PipelineRunner<?>>> getPipelineRunners() {
      return ImmutableList.of(RegisteredTestRunner.class);
    }
  }

  /** Test interface. */
  public interface RegisteredTestOptions extends PipelineOptions {
    Object getRegisteredExampleFooBar();

    void setRegisteredExampleFooBar(Object registeredExampleFooBar);
  }

  /**
   * A {@link PipelineOptionsRegistrar} to demonstrate default {@link PipelineOptions} registration.
   */
  @AutoService(PipelineOptionsRegistrar.class)
  public static class RegisteredTestOptionsRegistrar implements PipelineOptionsRegistrar {
    @Override
    public Iterable<Class<? extends PipelineOptions>> getPipelineOptions() {
      return ImmutableList.of(RegisteredTestOptions.class);
    }
  }

  @Test
  public void testRegistrationOfJacksonModulesForObjectMapper() throws Exception {
    JacksonIncompatibleOptions options =
        PipelineOptionsFactory.fromArgs("--jacksonIncompatible=\"testValue\"")
            .as(JacksonIncompatibleOptions.class);
    assertEquals("testValue", options.getJacksonIncompatible().value);
  }

  /** PipelineOptions used to test auto registration of Jackson modules. */
  public interface JacksonIncompatibleOptions extends PipelineOptions {
    JacksonIncompatible getJacksonIncompatible();

    void setJacksonIncompatible(JacksonIncompatible value);
  }

  /** A Jackson {@link Module} to test auto-registration of modules. */
  @AutoService(Module.class)
  public static class RegisteredTestModule extends SimpleModule {
    public RegisteredTestModule() {
      super("RegisteredTestModule");
      setMixInAnnotation(JacksonIncompatible.class, JacksonIncompatibleMixin.class);
    }
  }

  /** A class which Jackson does not know how to serialize/deserialize. */
  public static class JacksonIncompatible {
    private final String value;

    public JacksonIncompatible(String value) {
      this.value = value;
    }
  }

  /** A Jackson mixin used to add annotations to other classes. */
  @JsonDeserialize(using = JacksonIncompatibleDeserializer.class)
  @JsonSerialize(using = JacksonIncompatibleSerializer.class)
  public static final class JacksonIncompatibleMixin {}

  /** A Jackson deserializer for {@link JacksonIncompatible}. */
  public static class JacksonIncompatibleDeserializer
      extends JsonDeserializer<JacksonIncompatible> {

    @Override
    public JacksonIncompatible deserialize(
        JsonParser jsonParser, DeserializationContext deserializationContext)
        throws IOException, JsonProcessingException {
      return new JacksonIncompatible(jsonParser.readValueAs(String.class));
    }
  }

  /** A Jackson serializer for {@link JacksonIncompatible}. */
  public static class JacksonIncompatibleSerializer extends JsonSerializer<JacksonIncompatible> {

    @Override
    public void serialize(
        JacksonIncompatible jacksonIncompatible,
        JsonGenerator jsonGenerator,
        SerializerProvider serializerProvider)
        throws IOException, JsonProcessingException {
      jsonGenerator.writeString(jacksonIncompatible.value);
    }
  }

  /** Used to test that the thread context class loader is used when creating proxies. */
  public interface ClassLoaderTestOptions extends PipelineOptions {
    @Default.Boolean(true)
    @Description("A test option.")
    boolean isOption();

    void setOption(boolean b);
  }

  @Test
  public void testPipelineOptionsFactoryUsesTccl() throws Exception {
    final Thread thread = Thread.currentThread();
    final ClassLoader testClassLoader = thread.getContextClassLoader();
    final ClassLoader caseLoader =
        new InterceptingUrlClassLoader(
            testClassLoader, name -> name.toLowerCase(ROOT).contains("test"));
    thread.setContextClassLoader(caseLoader);
    PipelineOptionsFactory.resetCache();
    try {
      final PipelineOptions pipelineOptions = PipelineOptionsFactory.create();
      final Class optionType =
          caseLoader.loadClass(
              "org.apache.beam.sdk.options.PipelineOptionsFactoryTest$ClassLoaderTestOptions");
      final Object options = pipelineOptions.as(optionType);
      assertSame(caseLoader, options.getClass().getClassLoader());
      assertSame(optionType.getClassLoader(), options.getClass().getClassLoader());
      assertSame(testClassLoader, optionType.getInterfaces()[0].getClassLoader());
      assertTrue(Boolean.class.cast(optionType.getMethod("isOption").invoke(options)));
    } finally {
      thread.setContextClassLoader(testClassLoader);
      PipelineOptionsFactory.resetCache();
    }
  }

  @Test
  public void testDefaultMethodIgnoresDefaultImplementation() {
    OptionsWithDefaultMethod optsWithDefault =
        PipelineOptionsFactory.as(OptionsWithDefaultMethod.class);
    assertThat(optsWithDefault.getValue(), nullValue());

    optsWithDefault.setValue(12.25);
    assertThat(optsWithDefault.getValue(), equalTo(12.25));
  }

  /** Test interface. */
  public interface ExtendedOptionsWithDefault extends OptionsWithDefaultMethod {}

  @Test
  public void testDefaultMethodInExtendedClassIgnoresDefaultImplementation() {
    OptionsWithDefaultMethod extendedOptsWithDefault =
        PipelineOptionsFactory.as(ExtendedOptionsWithDefault.class);
    assertThat(extendedOptsWithDefault.getValue(), nullValue());

    extendedOptsWithDefault.setValue(Double.NEGATIVE_INFINITY);
    assertThat(extendedOptsWithDefault.getValue(), equalTo(Double.NEGATIVE_INFINITY));
  }

  /** Test interface. */
  public interface OptionsWithDefaultMethod extends PipelineOptions {
    default Number getValue() {
      return 1024;
    }

    void setValue(Number value);
  }

  @Test
  public void testStaticMethodsAreAllowed() {
    assertEquals(
        "value",
        OptionsWithStaticMethod.myStaticMethod(
            PipelineOptionsFactory.fromArgs("--myMethod=value").as(OptionsWithStaticMethod.class)));
  }

  /** Test interface. */
  public interface OptionsWithStaticMethod extends PipelineOptions {
    String getMyMethod();

    void setMyMethod(String value);

    static String myStaticMethod(OptionsWithStaticMethod o) {
      return o.getMyMethod();
    }
  }

  public static class SimpleParsedObject {
    public String value;

    public SimpleParsedObject(String value) {
      this.value = value;
    }
  }

  public interface OptionsWithParsing extends PipelineOptions {
    SimpleParsedObject getSimple();

    void setSimple(SimpleParsedObject value);
  }

  @Test
  public void testAutoQuoteStringArgumentsForComplexObjects() {
    OptionsWithParsing options =
        PipelineOptionsFactory.fromArgs("--simple=test").as(OptionsWithParsing.class);

    assertEquals("test", options.getSimple().value);
  }

  public static class ComplexType2 {
    public String value;

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (!(o instanceof ComplexType2)) {
        return false;
      }
      ComplexType2 that = (ComplexType2) o;
      return value.equals(that.value);
    }

    @Override
    public int hashCode() {
      return value.hashCode();
    }
  }

  public interface OptionsWithJsonDeserialize1 extends PipelineOptions {
    @JsonDeserialize(using = ComplexType2Deserializer1.class)
    @JsonSerialize(using = ComplexType2Serializer1.class)
    ComplexType2 getComplexType();

    void setComplexType(ComplexType2 value);
  }

  public interface OptionsWithJsonDeserialize2 extends PipelineOptions {
    @JsonDeserialize(using = ComplexType2Deserializer2.class)
    ComplexType2 getComplexType();

    void setComplexType(ComplexType2 value);
  }

  public static class ComplexType2Deserializer1 extends StdDeserializer<ComplexType2> {
    public ComplexType2Deserializer1() {
      super(ComplexType2.class);
    }

    @Override
    public ComplexType2 deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
      ComplexType2 ct = new ComplexType2();
      ct.value = p.getText();
      return ct;
    }
  }

  public static class ComplexType2Serializer1 extends StdSerializer<ComplexType2> {
    public ComplexType2Serializer1() {
      super(ComplexType2.class);
    }

    @Override
    public void serialize(ComplexType2 value, JsonGenerator gen, SerializerProvider provider)
        throws IOException {
      gen.writeString(value.value);
    }
  }

  public static class ComplexType2Deserializer2 extends StdDeserializer<ComplexType2> {
    public ComplexType2Deserializer2() {
      super(ComplexType2.class);
    }

    @Override
    public ComplexType2 deserialize(JsonParser p, DeserializationContext ctxt)
        throws IOException, JsonProcessingException {
      ComplexType2 ct = new ComplexType2();
      ct.value = p.getText();
      return ct;
    }
  }

  @Test
  public void testJsonDeserializeAttribute_NoConflict() {
    OptionsWithJsonDeserialize1 options =
        PipelineOptionsFactory.fromArgs("--complexType=test").as(OptionsWithJsonDeserialize1.class);

    assertEquals("test", options.getComplexType().value);
  }

  @Test
  public void testJsonDeserializeAttribute_Conflict() {
    OptionsWithJsonDeserialize1 options =
        PipelineOptionsFactory.fromArgs("--complexType=test").as(OptionsWithJsonDeserialize1.class);

    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class, () -> options.as(OptionsWithJsonDeserialize2.class));
    assertThat(
        thrown.getMessage(),
        containsString("Property [complexType] is marked with contradictory annotations"));
  }

  public interface InconsistentJsonDeserializeAttributes extends PipelineOptions {
    @JsonDeserialize()
    String getString();

    void setString(String value);
  }

  public interface InconsistentJsonSerializeAttributes extends PipelineOptions {
    @JsonSerialize()
    String getString();

    void setString(String value);
  }

  @Test
  public void testJsonDeserializeAttributeValidation() {
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                PipelineOptionsFactory.fromArgs("--string=test")
                    .as(InconsistentJsonDeserializeAttributes.class));
    assertThat(thrown.getMessage(), containsString("Property [string] had only @JsonDeserialize"));
  }

  @Test
  public void testJsonSerializeAttributeValidation() {
    IllegalArgumentException thrown =
        assertThrows(
            IllegalArgumentException.class,
            () ->
                PipelineOptionsFactory.fromArgs("--string=test")
                    .as(InconsistentJsonSerializeAttributes.class));
    assertThat(thrown.getMessage(), containsString("Property [string] had only @JsonSerialize"));
  }

  /** Test interface. */
  public interface TestDescribeOptions extends PipelineOptions {
    String getString();

    void setString(String value);

    @Description("integer property")
    Integer getInteger();

    void setInteger(Integer value);

    @Description("float number property")
    Float getFloat();

    void setFloat(Float value);

    @Description("simple boolean property")
    @Default.Boolean(true)
    boolean getBooleanSimple();

    void setBooleanSimple(boolean value);

    @Default.Boolean(false)
    Boolean getBooleanWrapper();

    void setBooleanWrapper(Boolean value);

    List<Integer> getList();

    void setList(List<Integer> value);
  }

  @Test
  public void testDescribe() {
    List<PipelineOptionDescriptor> described =
        PipelineOptionsFactory.describe(
            Sets.newHashSet(PipelineOptions.class, TestDescribeOptions.class));

    Map<String, PipelineOptionDescriptor> mapped = uniqueIndex(described, input -> input.getName());
    assertEquals("no duplicates", described.size(), mapped.size());

    Collection<PipelineOptionDescriptor> filtered =
        Collections2.filter(
            described, input -> input.getGroup().equals(TestDescribeOptions.class.getName()));
    assertEquals(6, filtered.size());
    mapped = uniqueIndex(filtered, input -> input.getName());

    PipelineOptionDescriptor listDesc = mapped.get("list");
    assertThat(listDesc, notNullValue());
    assertThat(listDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.ARRAY, listDesc.getType());
    assertThat(listDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor stringDesc = mapped.get("string");
    assertThat(stringDesc, notNullValue());
    assertThat(stringDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.STRING, stringDesc.getType());
    assertThat(stringDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor integerDesc = mapped.get("integer");
    assertThat(integerDesc, notNullValue());
    assertEquals("integer property", integerDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.INTEGER, integerDesc.getType());
    assertThat(integerDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor floatDesc = mapped.get("float");
    assertThat(integerDesc, notNullValue());
    assertEquals("float number property", floatDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.NUMBER, floatDesc.getType());
    assertThat(floatDesc.getDefaultValue(), isEmptyString());

    PipelineOptionDescriptor booleanSimpleDesc = mapped.get("boolean_simple");
    assertThat(booleanSimpleDesc, notNullValue());
    assertEquals("simple boolean property", booleanSimpleDesc.getDescription());
    assertEquals(PipelineOptionType.Enum.BOOLEAN, booleanSimpleDesc.getType());
    assertThat(booleanSimpleDesc.getDefaultValue(), equalTo("true"));

    PipelineOptionDescriptor booleanWrapperDesc = mapped.get("boolean_wrapper");
    assertThat(booleanWrapperDesc, notNullValue());
    assertThat(booleanWrapperDesc.getDescription(), isEmptyString());
    assertEquals(PipelineOptionType.Enum.BOOLEAN, booleanWrapperDesc.getType());
    assertThat(booleanWrapperDesc.getDefaultValue(), equalTo("false"));
  }
}
