/*
 * 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 org.apache.beam.sdk.testing.CrashingRunner;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link PipelineOptionsValidator}. */
@RunWith(JUnit4.class)
public class PipelineOptionsValidatorTest {
  @Rule public ExpectedException expectedException = ExpectedException.none();

  /** A test interface with an {@link Validation.Required} annotation. */
  public interface Required extends PipelineOptions {
    @Validation.Required
    @Description("Fake Description")
    String getObject();

    void setObject(String value);
  }

  @Test
  public void testWhenRequiredOptionIsSet() {
    Required required = PipelineOptionsFactory.as(Required.class);
    required.setRunner(CrashingRunner.class);
    required.setObject("blah");
    PipelineOptionsValidator.validate(Required.class, required);
  }

  @Test
  public void testWhenRequiredOptionIsSetAndCleared() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for "
            + "[public abstract java.lang.String org.apache.beam."
            + "sdk.options.PipelineOptionsValidatorTest$Required.getObject(), \"Fake Description\"].");

    Required required = PipelineOptionsFactory.as(Required.class);
    required.setObject("blah");
    required.setObject(null);
    PipelineOptionsValidator.validate(Required.class, required);
  }

  @Test
  public void testWhenRequiredOptionIsSetAndClearedCli() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for " + "[--object, \"Fake Description\"].");

    Required required =
        PipelineOptionsFactory.fromArgs(new String[] {"--object=blah"}).as(Required.class);
    required.setObject(null);
    PipelineOptionsValidator.validateCli(Required.class, required);
  }

  @Test
  public void testWhenRequiredOptionIsNeverSet() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for "
            + "[public abstract java.lang.String org.apache.beam."
            + "sdk.options.PipelineOptionsValidatorTest$Required.getObject(), \"Fake Description\"].");

    Required required = PipelineOptionsFactory.as(Required.class);
    PipelineOptionsValidator.validate(Required.class, required);
  }

  @Test
  public void testWhenRequiredOptionIsNeverSetCli() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for " + "[--object, \"Fake Description\"].");

    Required required = PipelineOptionsFactory.fromArgs(new String[] {}).as(Required.class);
    PipelineOptionsValidator.validateCli(Required.class, required);
  }

  @Test
  public void testWhenRequiredOptionIsNeverSetOnSuperInterface() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for "
            + "[public abstract java.lang.String org.apache.beam."
            + "sdk.options.PipelineOptionsValidatorTest$Required.getObject(), \"Fake Description\"].");

    PipelineOptions options = PipelineOptionsFactory.create();
    PipelineOptionsValidator.validate(Required.class, options);
  }

  @Test
  public void testWhenRequiredOptionIsNeverSetOnSuperInterfaceCli() {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for " + "[--object, \"Fake Description\"].");

    PipelineOptions options = PipelineOptionsFactory.fromArgs(new String[] {}).create();
    PipelineOptionsValidator.validateCli(Required.class, options);
  }

  /** A test interface that overrides the parent's method. */
  public interface SubClassValidation extends Required {
    @Override
    String getObject();

    @Override
    void setObject(String value);
  }

  @Test
  public void testValidationOnOverriddenMethods() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for "
            + "[public abstract java.lang.String org.apache.beam."
            + "sdk.options.PipelineOptionsValidatorTest$Required.getObject(), \"Fake Description\"].");

    SubClassValidation required = PipelineOptionsFactory.as(SubClassValidation.class);
    PipelineOptionsValidator.validate(Required.class, required);
  }

  @Test
  public void testValidationOnOverriddenMethodsCli() throws Exception {
    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage(
        "Missing required value for " + "[--object, \"Fake Description\"].");

    SubClassValidation required =
        PipelineOptionsFactory.fromArgs(new String[] {}).as(SubClassValidation.class);
    PipelineOptionsValidator.validateCli(Required.class, required);
  }

  /** A test interface with a required group. */
  public interface GroupRequired extends PipelineOptions {
    @Validation.Required(groups = {"ham"})
    String getFoo();

    void setFoo(String foo);

    @Validation.Required(groups = {"ham"})
    String getBar();

    void setBar(String bar);
  }

  @Test
  public void testWhenOneOfRequiredGroupIsSetIsValid() {
    GroupRequired groupRequired = PipelineOptionsFactory.as(GroupRequired.class);
    groupRequired.setFoo("foo");
    groupRequired.setBar(null);
    groupRequired.setRunner(CrashingRunner.class);

    PipelineOptionsValidator.validate(GroupRequired.class, groupRequired);

    // Symmetric
    groupRequired.setFoo(null);
    groupRequired.setBar("bar");
    PipelineOptionsValidator.validate(GroupRequired.class, groupRequired);
  }

  @Test
  public void testWhenNoneOfRequiredGroupIsSetThrowsException() {
    GroupRequired groupRequired = PipelineOptionsFactory.as(GroupRequired.class);
    groupRequired.setRunner(CrashingRunner.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Missing required value for group [ham]");
    expectedException.expectMessage("properties");
    expectedException.expectMessage("getFoo");
    expectedException.expectMessage("getBar");

    PipelineOptionsValidator.validate(GroupRequired.class, groupRequired);
  }

  /** A test interface with a member in multiple required groups. */
  public interface MultiGroupRequired extends PipelineOptions {
    @Validation.Required(groups = {"spam", "ham"})
    String getFoo();

    void setFoo(String foo);

    @Validation.Required(groups = {"spam"})
    String getBar();

    void setBar(String bar);

    @Validation.Required(groups = {"ham"})
    String getBaz();

    void setBaz(String baz);
  }

  @Test
  public void testWhenOneOfMultipleRequiredGroupsIsSetIsValid() {
    MultiGroupRequired multiGroupRequired = PipelineOptionsFactory.as(MultiGroupRequired.class);

    multiGroupRequired.setRunner(CrashingRunner.class);
    multiGroupRequired.setFoo("eggs");

    PipelineOptionsValidator.validate(MultiGroupRequired.class, multiGroupRequired);
  }

  private interface LeftOptions extends PipelineOptions {
    @Validation.Required(groups = {"left"})
    String getFoo();

    void setFoo(String foo);

    @Validation.Required(groups = {"left"})
    String getLeft();

    void setLeft(String left);

    @Validation.Required(groups = {"both"})
    String getBoth();

    void setBoth(String both);
  }

  private interface RightOptions extends PipelineOptions {
    @Validation.Required(groups = {"right"})
    String getFoo();

    void setFoo(String foo);

    @Validation.Required(groups = {"right"})
    String getRight();

    void setRight(String right);

    @Validation.Required(groups = {"both"})
    String getBoth();

    void setBoth(String both);
  }

  private interface JoinedOptions extends LeftOptions, RightOptions {}

  @Test
  public void testWhenOptionIsDefinedInMultipleSuperInterfacesAndIsNotPresentFailsRequirement() {
    RightOptions rightOptions = PipelineOptionsFactory.as(RightOptions.class);
    rightOptions.setBoth("foo");
    rightOptions.setRunner(CrashingRunner.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("Missing required value for group");
    expectedException.expectMessage("getFoo");

    PipelineOptionsValidator.validate(JoinedOptions.class, rightOptions);
  }

  @Test
  public void testWhenOptionIsDefinedInMultipleSuperInterfacesMeetsGroupRequirement() {
    RightOptions rightOpts = PipelineOptionsFactory.as(RightOptions.class);
    rightOpts.setFoo("true");
    rightOpts.setBoth("bar");

    LeftOptions leftOpts = PipelineOptionsFactory.as(LeftOptions.class);
    leftOpts.setFoo("Untrue");
    leftOpts.setBoth("Raise the");

    rightOpts.setRunner(CrashingRunner.class);
    leftOpts.setRunner(CrashingRunner.class);
    PipelineOptionsValidator.validate(JoinedOptions.class, rightOpts);
    PipelineOptionsValidator.validate(JoinedOptions.class, leftOpts);
  }

  @Test
  public void testWhenOptionIsDefinedOnOtherOptionsClassMeetsGroupRequirement() {
    RightOptions rightOpts = PipelineOptionsFactory.as(RightOptions.class);
    rightOpts.setFoo("true");
    rightOpts.setBoth("bar");

    LeftOptions leftOpts = PipelineOptionsFactory.as(LeftOptions.class);
    leftOpts.setFoo("Untrue");
    leftOpts.setBoth("Raise the");

    rightOpts.setRunner(CrashingRunner.class);
    leftOpts.setRunner(CrashingRunner.class);
    PipelineOptionsValidator.validate(RightOptions.class, leftOpts);
    PipelineOptionsValidator.validate(LeftOptions.class, rightOpts);
  }

  @Test
  public void testWhenOptionIsDefinedOnMultipleInterfacesOnlyListedOnceWhenNotPresent() {
    JoinedOptions options = PipelineOptionsFactory.as(JoinedOptions.class);
    options.setFoo("Hello");
    options.setRunner(CrashingRunner.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("required value for group [both]");
    expectedException.expectMessage("properties [getBoth()]");

    PipelineOptionsValidator.validate(JoinedOptions.class, options);
  }

  private interface SuperOptions extends PipelineOptions {
    @Validation.Required(groups = {"super"})
    String getFoo();

    void setFoo(String foo);

    @Validation.Required(groups = {"sub"})
    String getBar();

    void setBar(String bar);

    @Validation.Required(groups = {"otherSuper"})
    String getSuperclassObj();

    void setSuperclassObj(String sup);
  }

  private interface SubOptions extends SuperOptions {
    @Override
    @Validation.Required(groups = {"sub"})
    String getFoo();

    @Override
    void setFoo(String foo);

    @Override
    String getSuperclassObj();

    @Override
    void setSuperclassObj(String sup);
  }

  @Test
  public void testSuperInterfaceRequiredOptionsAlsoRequiredInSubInterface() {
    SubOptions subOpts = PipelineOptionsFactory.as(SubOptions.class);
    subOpts.setFoo("Bar");
    subOpts.setRunner(CrashingRunner.class);

    expectedException.expect(IllegalArgumentException.class);
    expectedException.expectMessage("otherSuper");
    expectedException.expectMessage("Missing required value");
    expectedException.expectMessage("getSuperclassObj");

    PipelineOptionsValidator.validate(SubOptions.class, subOpts);
  }

  @Test
  public void
      testSuperInterfaceGroupIsInAdditionToSubInterfaceGroupOnlyWhenValidatingSuperInterface() {
    SubOptions opts = PipelineOptionsFactory.as(SubOptions.class);
    opts.setFoo("Foo");
    opts.setSuperclassObj("Hello world");
    opts.setRunner(CrashingRunner.class);

    // Valid SubOptions, but invalid SuperOptions
    PipelineOptionsValidator.validate(SubOptions.class, opts);

    expectedException.expectMessage("sub");
    expectedException.expectMessage("Missing required value");
    expectedException.expectMessage("getBar");
    PipelineOptionsValidator.validate(SuperOptions.class, opts);
  }

  @Test
  public void testSuperInterfaceRequiredOptionsSatisfiedBySubInterface() {
    SubOptions subOpts = PipelineOptionsFactory.as(SubOptions.class);
    subOpts.setFoo("bar");
    subOpts.setBar("bar");
    subOpts.setSuperclassObj("SuperDuper");
    subOpts.setRunner(CrashingRunner.class);

    PipelineOptionsValidator.validate(SubOptions.class, subOpts);
    PipelineOptionsValidator.validate(SuperOptions.class, subOpts);
  }
}
