// Copyright 2014 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.devtools.common.options;

import static com.google.common.truth.Truth.assertThat;
import static com.google.devtools.common.options.OptionsParser.newOptionsParser;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.common.options.Converters.CommaSeparatedOptionListConverter;
import com.google.devtools.common.options.OptionsParser.OptionValueDescription;
import com.google.devtools.common.options.OptionsParser.UnparsedOptionValueDescription;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Tests {@link OptionsParser}.
 */
@RunWith(JUnit4.class)
public class OptionsParserTest {

  public static class ExampleFoo extends OptionsBase {

    @Option(name = "foo",
            category = "one",
            defaultValue = "defaultFoo")
    public String foo;

    @Option(name = "bar",
            category = "two",
            defaultValue = "42")
    public int bar;

    @Option(name = "bing",
            category = "one",
            defaultValue = "",
            allowMultiple = true)
    public List<String> bing;

    @Option(name = "bang",
            category = "one",
            defaultValue = "",
            converter = StringConverter.class,
            allowMultiple = true)
    public List<String> bang;

    @Option(name = "nodoc",
        category = "undocumented",
        defaultValue = "",
        allowMultiple = false)
    public String nodoc;
  }

  public static class ExampleBaz extends OptionsBase {

    @Option(name = "baz",
            category = "one",
            defaultValue = "defaultBaz")
    public String baz;
  }

  /** Subclass of an options class. */
  public static class ExampleBazSubclass extends ExampleBaz {

    @Option(name = "baz_subclass",
            category = "one",
            defaultValue = "defaultBazSubclass")
    public String bazSubclass;
  }

  /**
   * Example with empty to null string converter
   */
  public static class ExampleBoom extends OptionsBase {
    @Option(name = "boom",
            defaultValue = "defaultBoom",
            converter = EmptyToNullStringConverter.class)
    public String boom;
  }

  public static class StringConverter implements Converter<String> {
    @Override
    public String convert(String input) {
      return input;
    }
    @Override
    public String getTypeDescription() {
      return "a string";
    }
  }

  /**
   * A converter that defaults to null if the input is the empty string
   */
  public static class EmptyToNullStringConverter extends StringConverter {
    @Override
    public String convert(String input) {
      return input.isEmpty() ? null : input;
    }
  }

  @Test
  public void parseWithMultipleOptionsInterfaces()
      throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    parser.parse("--baz=oops", "--bar", "17");
    ExampleFoo foo = parser.getOptions(ExampleFoo.class);
    assertEquals("defaultFoo", foo.foo);
    assertEquals(17, foo.bar);
    ExampleBaz baz = parser.getOptions(ExampleBaz.class);
    assertEquals("oops", baz.baz);
  }

  @Test
  public void parseWithOptionsInheritance() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleBazSubclass.class);
    parser.parse("--baz_subclass=cat", "--baz=dog");
    ExampleBazSubclass subclassOptions = parser.getOptions(ExampleBazSubclass.class);
    assertThat(subclassOptions.bazSubclass).isEqualTo("cat");
    assertThat(subclassOptions.baz).isEqualTo("dog");
    ExampleBaz options = parser.getOptions(ExampleBaz.class);
    // This is a test showcasing the lack of functionality for retrieving parsed options at a
    // superclass type class type. If there's a need for this functionality, we can add it later.
    assertThat(options).isNull();
  }

  @Test
  public void parserWithUnknownOption() {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    try {
      parser.parse("--unknown", "option");
      fail();
    } catch (OptionsParsingException e) {
      assertEquals("--unknown", e.getInvalidArgument());
      assertEquals("Unrecognized option: --unknown", e.getMessage());
    }
    assertEquals(Collections.<String>emptyList(), parser.getResidue());
  }

  @Test
  public void parserWithSingleDashOption() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    try {
      parser.parse("-baz=oops", "-bar", "17");
      fail();
    } catch (OptionsParsingException expected) {}

    parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    parser.setAllowSingleDashLongOptions(true);
    parser.parse("-baz=oops", "-bar", "17");
    ExampleFoo foo = parser.getOptions(ExampleFoo.class);
    assertEquals("defaultFoo", foo.foo);
    assertEquals(17, foo.bar);
    ExampleBaz baz = parser.getOptions(ExampleBaz.class);
    assertEquals("oops", baz.baz);
  }

  @Test
  public void parsingFailsWithUnknownOptions() {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    List<String> unknownOpts = asList("--unknown", "option", "--more_unknowns");
    try {
      parser.parse(unknownOpts);
      fail();
    } catch (OptionsParsingException e) {
      assertEquals("--unknown", e.getInvalidArgument());
      assertEquals("Unrecognized option: --unknown", e.getMessage());
      assertNotNull(parser.getOptions(ExampleFoo.class));
      assertNotNull(parser.getOptions(ExampleBaz.class));
    }
  }

  @Test
  public void parseKnownAndUnknownOptions() {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    List<String> opts = asList("--bar", "17", "--unknown", "option");
    try {
      parser.parse(opts);
      fail();
    } catch (OptionsParsingException e) {
      assertEquals("--unknown", e.getInvalidArgument());
      assertEquals("Unrecognized option: --unknown", e.getMessage());
      assertNotNull(parser.getOptions(ExampleFoo.class));
      assertNotNull(parser.getOptions(ExampleBaz.class));
    }
  }

  @Test
  public void parseAndOverrideWithEmptyStringToObtainNullValueInOption()
      throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleBoom.class);
    // Override --boom value to the empty string
    parser.parse("--boom=");
    ExampleBoom boom = parser.getOptions(ExampleBoom.class);
    // The converted value is intentionally null since boom uses the EmptyToNullStringConverter
    assertNull(boom.boom);
  }

  public static class CategoryTest extends OptionsBase {
    @Option(name = "swiss_bank_account_number",
            category = "undocumented", // Not printed in usage messages!
            defaultValue = "123456789")
    public int swissBankAccountNumber;

    @Option(name = "student_bank_account_number",
            category = "one",
            defaultValue = "987654321")
    public int studentBankAccountNumber;
  }

  @Test
  public void getOptionsAndGetResidueWithNoCallToParse() {
    // With no call to parse(), all options are at default values, and there's
    // no reside.
    assertEquals("defaultFoo",
                 newOptionsParser(ExampleFoo.class).
                 getOptions(ExampleFoo.class).foo);
    assertEquals(Collections.<String>emptyList(),
                 newOptionsParser(ExampleFoo.class).getResidue());
  }

  @Test
  public void parserCanBeCalledRepeatedly() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleFoo.class);
    parser.parse("--foo", "foo1");
    assertEquals("foo1", parser.getOptions(ExampleFoo.class).foo);
    parser.parse();
    assertEquals("foo1", parser.getOptions(ExampleFoo.class).foo); // no change
    parser.parse("--foo", "foo2");
    assertEquals("foo2", parser.getOptions(ExampleFoo.class).foo); // updated
  }

  @Test
  public void multipleOccuringOption() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleFoo.class);
    parser.parse("--bing", "abcdef", "--foo", "foo1", "--bing", "123456" );
    assertThat(parser.getOptions(ExampleFoo.class).bing).containsExactly("abcdef", "123456");
  }

  @Test
  public void multipleOccurringOptionWithConverter() throws OptionsParsingException {
    // --bang is the same as --bing except that it has a "converter" specified.
    // This test also tests option values with embedded commas and spaces.
    OptionsParser parser = newOptionsParser(ExampleFoo.class);
    parser.parse("--bang", "abc,def ghi", "--foo", "foo1", "--bang", "123456" );
    assertThat(parser.getOptions(ExampleFoo.class).bang).containsExactly("abc,def ghi", "123456");
  }

  @Test
  public void parserIgnoresOptionsAfterMinusMinus()
      throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    parser.parse("--foo", "well", "--baz", "here", "--", "--bar", "ignore");
    ExampleFoo foo = parser.getOptions(ExampleFoo.class);
    ExampleBaz baz = parser.getOptions(ExampleBaz.class);
    assertEquals("well", foo.foo);
    assertEquals("here", baz.baz);
    assertEquals(42, foo.bar); // the default!
    assertEquals(asList("--bar", "ignore"), parser.getResidue());
  }

  @Test
  public void parserThrowsExceptionIfResidueIsNotAllowed() {
    OptionsParser parser = newOptionsParser(ExampleFoo.class);
    parser.setAllowResidue(false);
    try {
      parser.parse("residue", "is", "not", "OK");
      fail();
    } catch (OptionsParsingException e) {
      assertEquals("Unrecognized arguments: residue is not OK", e.getMessage());
    }
  }

  @Test
  public void multipleCallsToParse() throws Exception {
    OptionsParser parser = newOptionsParser(ExampleFoo.class);
    parser.setAllowResidue(true);
    parser.parse("--foo", "one", "--bar", "43", "unknown1");
    parser.parse("--foo", "two", "unknown2");
    ExampleFoo foo = parser.getOptions(ExampleFoo.class);
    assertEquals("two", foo.foo); // second call takes precedence
    assertEquals(43, foo.bar);
    assertEquals(Arrays.asList("unknown1", "unknown2"), parser.getResidue());
  }

  // Regression test for a subtle bug!  The toString of each options interface
  // instance was printing out key=value pairs for all flags in the
  // OptionsParser, not just those belonging to the specific interface type.
  @Test
  public void toStringDoesntIncludeFlagsForOtherOptionsInParserInstance()
      throws Exception {
    OptionsParser parser = newOptionsParser(ExampleFoo.class, ExampleBaz.class);
    parser.parse("--foo", "foo", "--bar", "43", "--baz", "baz");

    String fooString = parser.getOptions(ExampleFoo.class).toString();
    if (!fooString.contains("foo=foo") ||
        !fooString.contains("bar=43") ||
        !fooString.contains("ExampleFoo") ||
        fooString.contains("baz=baz")) {
      fail("ExampleFoo.toString() is incorrect: " + fooString);
    }

    String bazString = parser.getOptions(ExampleBaz.class).toString();
    if (!bazString.contains("baz=baz") ||
        !bazString.contains("ExampleBaz") ||
        bazString.contains("foo=foo") ||
        bazString.contains("bar=43")) {
      fail("ExampleBaz.toString() is incorrect: " + bazString);
    }
  }

  // Regression test for another subtle bug!  The toString was printing all the
  // explicitly-specified options, even if they were at their default values,
  // causing toString equivalence to diverge from equals().
  @Test
  public void toStringIsIndependentOfExplicitCommandLineOptions() throws Exception {
    ExampleFoo foo1 = Options.parse(ExampleFoo.class).getOptions();
    ExampleFoo foo2 = Options.parse(ExampleFoo.class, "--bar", "42").getOptions();
    assertEquals(foo1, foo2);
    assertEquals(foo1.toString(), foo2.toString());

    Map<String, Object> expectedMap = new ImmutableMap.Builder<String, Object>().
        put("bing", Collections.emptyList()).
        put("bar", 42).
        put("nodoc", "").
        put("bang", Collections.emptyList()).
        put("foo", "defaultFoo").build();

    assertEquals(expectedMap, foo1.asMap());
    assertEquals(expectedMap, foo2.asMap());
  }

  // Regression test for yet another subtle bug!  The inherited options weren't
  // being printed by toString.  One day, a real rain will come and wash all
  // this scummy code off the streets.
  public static class DerivedBaz extends ExampleBaz {
    @Option(name = "derived", defaultValue = "defaultDerived")
    public String derived;
  }

  @Test
  public void toStringPrintsInheritedOptionsToo_Duh() throws Exception {
    DerivedBaz derivedBaz = Options.parse(DerivedBaz.class).getOptions();
    String derivedBazString = derivedBaz.toString();
    if (!derivedBazString.contains("derived=defaultDerived") ||
        !derivedBazString.contains("baz=defaultBaz")) {
      fail("DerivedBaz.toString() is incorrect: " + derivedBazString);
    }
  }

  // Tests for new default value override mechanism
  public static class CustomOptions extends OptionsBase {
    @Option(name = "simple",
        category = "custom",
        defaultValue = "simple default")
    public String simple;

    @Option(name = "multipart_name",
        category = "custom",
        defaultValue = "multipart default")
    public String multipartName;
  }

  public void assertDefaultStringsForCustomOptions() throws OptionsParsingException {
    CustomOptions options = Options.parse(CustomOptions.class).getOptions();
    assertEquals("simple default", options.simple);
    assertEquals("multipart default", options.multipartName);
  }

  public static class NullTestOptions extends OptionsBase {
    @Option(name = "simple",
            defaultValue = "null")
    public String simple;
  }

  @Test
  public void defaultNullStringGivesNull() throws Exception {
    NullTestOptions options = Options.parse(NullTestOptions.class).getOptions();
    assertNull(options.simple);
  }

  public static class ImplicitDependencyOptions extends OptionsBase {
    @Option(name = "first",
            implicitRequirements = "--second=second",
            defaultValue = "null")
    public String first;

    @Option(name = "second",
        implicitRequirements = "--third=third",
        defaultValue = "null")
    public String second;

    @Option(name = "third",
        defaultValue = "null")
    public String third;
  }

  @Test
  public void implicitDependencyHasImplicitDependency() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first=first"));
    assertEquals("first", parser.getOptions(ImplicitDependencyOptions.class).first);
    assertEquals("second", parser.getOptions(ImplicitDependencyOptions.class).second);
    assertEquals("third", parser.getOptions(ImplicitDependencyOptions.class).third);
  }

  public static class BadImplicitDependencyOptions extends OptionsBase {
    @Option(name = "first",
            implicitRequirements = "xxx",
            defaultValue = "null")
    public String first;
  }

  @Test
  public void badImplicitDependency() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(BadImplicitDependencyOptions.class);
    try {
      parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first=first"));
    } catch (AssertionError e) {
      /* Expected error. */
      return;
    }
    fail();
  }

  public static class BadExpansionOptions extends OptionsBase {
    @Option(name = "first",
            expansion = { "xxx" },
            defaultValue = "null")
    public Void first;
  }

  @Test
  public void badExpansionOptions() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(BadExpansionOptions.class);
    try {
      parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first"));
    } catch (AssertionError e) {
      /* Expected error. */
      return;
    }
    fail();
  }

  public static class ExpansionOptions extends OptionsBase {
    @Option(name = "first",
            expansion = { "--second=first" },
            defaultValue = "null")
    public Void first;

    @Option(name = "second",
            defaultValue = "null")
    public String second;
  }

  @Test
  public void overrideExpansionWithExplicit() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first", "--second=second"));
    ExpansionOptions options = parser.getOptions(ExpansionOptions.class);
    assertEquals("second", options.second);
    assertEquals(0, parser.getWarnings().size());
  }

  @Test
  public void overrideExplicitWithExpansion() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ExpansionOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--second=second", "--first"));
    ExpansionOptions options = parser.getOptions(ExpansionOptions.class);
    assertEquals("first", options.second);
  }

  @Test
  public void overrideWithHigherPriority() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class);
    parser.parse(OptionPriority.RC_FILE, null, Arrays.asList("--simple=a"));
    assertEquals("a", parser.getOptions(NullTestOptions.class).simple);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--simple=b"));
    assertEquals("b", parser.getOptions(NullTestOptions.class).simple);
  }

  @Test
  public void overrideWithLowerPriority() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--simple=a"));
    assertEquals("a", parser.getOptions(NullTestOptions.class).simple);
    parser.parse(OptionPriority.RC_FILE, null, Arrays.asList("--simple=b"));
    assertEquals("a", parser.getOptions(NullTestOptions.class).simple);
  }

  @Test
  public void getOptionValueDescriptionWithNonExistingOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class);
    try {
      parser.getOptionValueDescription("notexisting");
      fail();
    } catch (IllegalArgumentException e) {
      /* Expected exception. */
    }
  }

  @Test
  public void getOptionValueDescriptionWithoutValue() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class);
    assertNull(parser.getOptionValueDescription("simple"));
  }

  @Test
  public void getOptionValueDescriptionWithValue() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NullTestOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, "my description",
        Arrays.asList("--simple=abc"));
    OptionValueDescription result = parser.getOptionValueDescription("simple");
    assertNotNull(result);
    assertEquals("simple", result.getName());
    assertEquals("abc", result.getValue());
    assertEquals(OptionPriority.COMMAND_LINE, result.getPriority());
    assertEquals("my description", result.getSource());
    assertNull(result.getImplicitDependant());
    assertFalse(result.isImplicitDependency());
    assertNull(result.getExpansionParent());
    assertFalse(result.isExpansion());
  }

  public static class ImplicitDependencyWarningOptions extends OptionsBase {
    @Option(name = "first",
            implicitRequirements = "--second=second",
            defaultValue = "null")
    public String first;

    @Option(name = "second",
        defaultValue = "null")
    public String second;

    @Option(name = "third",
            implicitRequirements = "--second=third",
            defaultValue = "null")
    public String third;
  }

  @Test
  public void warningForImplicitOverridingExplicitOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class);
    parser.parse("--second=second", "--first=first");
    assertThat(parser.getWarnings())
        .containsExactly("Option 'second' is implicitly defined by "
                         + "option 'first'; the implicitly set value overrides the previous one");
  }

  @Test
  public void warningForExplicitOverridingImplicitOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class);
    parser.parse("--first=first");
    assertThat(parser.getWarnings()).isEmpty();
    parser.parse("--second=second");
    assertThat(parser.getWarnings())
        .containsExactly("A new value for option 'second' overrides a"
                         + " previous implicit setting of that option by option 'first'");
  }

  @Test
  public void warningForExplicitOverridingImplicitOptionInSameCall() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class);
    parser.parse("--first=first", "--second=second");
    assertThat(parser.getWarnings())
        .containsExactly("Option 'second' is implicitly defined by "
                         + "option 'first'; the implicitly set value overrides the previous one");
  }

  @Test
  public void warningForImplicitOverridingImplicitOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ImplicitDependencyWarningOptions.class);
    parser.parse("--first=first");
    assertThat(parser.getWarnings()).isEmpty();
    parser.parse("--third=third");
    assertThat(parser.getWarnings())
        .containsExactly("Option 'second' is implicitly defined by both "
                         + "option 'first' and option 'third'");
  }

  public static class WarningOptions extends OptionsBase {
    @Deprecated
    @Option(name = "first",
            defaultValue = "null")
    public Void first;

    @Deprecated
    @Option(name = "second",
            allowMultiple = true,
            defaultValue = "null")
    public List<String> second;

    @Deprecated
    @Option(name = "third",
            expansion = "--fourth=true",
            abbrev = 't',
            defaultValue = "null")
    public Void third;

    @Option(name = "fourth",
            defaultValue = "false")
    public boolean fourth;
  }

  @Test
  public void deprecationWarning() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first"));
    assertEquals(Arrays.asList("Option 'first' is deprecated"), parser.getWarnings());
  }

  @Test
  public void deprecationWarningForListOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--second=a"));
    assertEquals(Arrays.asList("Option 'second' is deprecated"), parser.getWarnings());
  }

  @Test
  public void deprecationWarningForExpansionOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--third"));
    assertEquals(Arrays.asList("Option 'third' is deprecated"), parser.getWarnings());
    assertTrue(parser.getOptions(WarningOptions.class).fourth);
  }

  @Test
  public void deprecationWarningForAbbreviatedExpansionOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(WarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("-t"));
    assertEquals(Arrays.asList("Option 'third' is deprecated"), parser.getWarnings());
    assertTrue(parser.getOptions(WarningOptions.class).fourth);
  }

  public static class NewWarningOptions extends OptionsBase {
    @Option(name = "first",
            defaultValue = "null",
            deprecationWarning = "it's gone")
    public Void first;

    @Option(name = "second",
            allowMultiple = true,
            defaultValue = "null",
            deprecationWarning = "sorry, no replacement")
    public List<String> second;

    @Option(name = "third",
            expansion = "--fourth=true",
            defaultValue = "null",
            deprecationWarning = "use --forth instead")
    public Void third;

    @Option(name = "fourth",
            defaultValue = "false")
    public boolean fourth;
  }

  @Test
  public void newDeprecationWarning() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--first"));
    assertEquals(Arrays.asList("Option 'first' is deprecated: it's gone"), parser.getWarnings());
  }

  @Test
  public void newDeprecationWarningForListOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--second=a"));
    assertEquals(Arrays.asList("Option 'second' is deprecated: sorry, no replacement"),
        parser.getWarnings());
  }

  @Test
  public void newDeprecationWarningForExpansionOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(NewWarningOptions.class);
    parser.parse(OptionPriority.COMMAND_LINE, null, Arrays.asList("--third"));
    assertEquals(Arrays.asList("Option 'third' is deprecated: use --forth instead"),
        parser.getWarnings());
    assertTrue(parser.getOptions(NewWarningOptions.class).fourth);
  }

  public static class ExpansionWarningOptions extends OptionsBase {
    @Option(name = "first",
            expansion = "--second=other",
            defaultValue = "null")
    public Void first;

    @Option(name = "second",
            defaultValue = "null")
    public String second;
  }

  @Test
  public void warningForExpansionOverridingExplicitOption() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ExpansionWarningOptions.class);
    parser.parse("--second=second", "--first");
    assertThat(parser.getWarnings())
        .containsExactly("The option 'first' was expanded and now overrides a "
                         + "previous explicitly specified option 'second'");
  }

  public static class InvalidOptionConverter extends OptionsBase {
    @Option(name = "foo",
            converter = StringConverter.class,
            defaultValue = "1")
    public Integer foo;
  }

  @Test
  public void errorForInvalidOptionConverter() throws Exception {
    try {
      OptionsParser.newOptionsParser(InvalidOptionConverter.class);
    } catch (AssertionError e) {
      // Expected exception
      return;
    }
    fail();
  }

  public static class InvalidListOptionConverter extends OptionsBase {
    @Option(name = "foo",
            converter = StringConverter.class,
            defaultValue = "1",
            allowMultiple = true)
    public List<Integer> foo;
  }

  @Test
  public void errorForInvalidListOptionConverter() throws Exception {
    try {
      OptionsParser.newOptionsParser(InvalidListOptionConverter.class);
    } catch (AssertionError e) {
      // Expected exception
      return;
    }
    fail();
  }

  // This test is here to make sure that nobody accidentally changes the
  // order of the enum values and breaks the implicit assumptions elsewhere
  // in the code.
  @Test
  public void optionPrioritiesAreCorrectlyOrdered() throws Exception {
    assertEquals(6, OptionPriority.values().length);
    assertThat(OptionPriority.DEFAULT).isLessThan(OptionPriority.COMPUTED_DEFAULT);
    assertThat(OptionPriority.COMPUTED_DEFAULT).isLessThan(OptionPriority.RC_FILE);
    assertThat(OptionPriority.RC_FILE).isLessThan(OptionPriority.COMMAND_LINE);
    assertThat(OptionPriority.COMMAND_LINE).isLessThan(OptionPriority.INVOCATION_POLICY);
    assertThat(OptionPriority.INVOCATION_POLICY).isLessThan(OptionPriority.SOFTWARE_REQUIREMENT);
  }

  public static class IntrospectionExample extends OptionsBase {
    @Option(name = "alpha",
            category = "one",
            defaultValue = "alpha")
    public String alpha;

    @Option(name = "beta",
            category = "one",
            defaultValue = "beta")
    public String beta;

    @Option(name = "gamma",
        category = "undocumented",
        defaultValue = "gamma")
    public String gamma;

    @Option(name = "delta",
        category = "undocumented",
        defaultValue = "delta")
    public String delta;

    @Option(name = "echo",
        category = "hidden",
        defaultValue = "echo")
    public String echo;
  }

  @Test
  public void asListOfUnparsedOptions() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class);
    parser.parse(OptionPriority.COMMAND_LINE, "source",
        Arrays.asList("--alpha=one", "--gamma=two", "--echo=three"));
    List<UnparsedOptionValueDescription> result = parser.asListOfUnparsedOptions();
    assertNotNull(result);
    assertEquals(3, result.size());

    assertEquals("alpha", result.get(0).getName());
    assertEquals(true, result.get(0).isDocumented());
    assertEquals(false, result.get(0).isHidden());
    assertEquals("one", result.get(0).getUnparsedValue());
    assertEquals("source", result.get(0).getSource());
    assertEquals(OptionPriority.COMMAND_LINE, result.get(0).getPriority());

    assertEquals("gamma", result.get(1).getName());
    assertEquals(false, result.get(1).isDocumented());
    assertEquals(false, result.get(1).isHidden());
    assertEquals("two", result.get(1).getUnparsedValue());
    assertEquals("source", result.get(1).getSource());
    assertEquals(OptionPriority.COMMAND_LINE, result.get(1).getPriority());

    assertEquals("echo", result.get(2).getName());
    assertEquals(false, result.get(2).isDocumented());
    assertEquals(true, result.get(2).isHidden());
    assertEquals("three", result.get(2).getUnparsedValue());
    assertEquals("source", result.get(2).getSource());
    assertEquals(OptionPriority.COMMAND_LINE, result.get(2).getPriority());
  }

  @Test
  public void asListOfExplicitOptions() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class);
    parser.parse(OptionPriority.COMMAND_LINE, "source",
        Arrays.asList("--alpha=one", "--gamma=two"));
    List<UnparsedOptionValueDescription> result = parser.asListOfExplicitOptions();
    assertNotNull(result);
    assertEquals(2, result.size());

    assertEquals("alpha", result.get(0).getName());
    assertEquals(true, result.get(0).isDocumented());
    assertEquals("one", result.get(0).getUnparsedValue());
    assertEquals("source", result.get(0).getSource());
    assertEquals(OptionPriority.COMMAND_LINE, result.get(0).getPriority());

    assertEquals("gamma", result.get(1).getName());
    assertEquals(false, result.get(1).isDocumented());
    assertEquals("two", result.get(1).getUnparsedValue());
    assertEquals("source", result.get(1).getSource());
    assertEquals(OptionPriority.COMMAND_LINE, result.get(1).getPriority());
  }

  private void assertOptionValue(String expectedName, Object expectedValue,
      OptionPriority expectedPriority, String expectedSource,
      OptionValueDescription actual) {
    assertNotNull(actual);
    assertEquals(expectedName, actual.getName());
    assertEquals(expectedValue, actual.getValue());
    assertEquals(expectedPriority, actual.getPriority());
    assertEquals(expectedSource, actual.getSource());
  }

  @Test
  public void asListOfEffectiveOptions() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(IntrospectionExample.class);
    parser.parse(OptionPriority.COMMAND_LINE, "source",
        Arrays.asList("--alpha=one", "--gamma=two"));
    List<OptionValueDescription> result = parser.asListOfEffectiveOptions();
    assertNotNull(result);
    assertEquals(5, result.size());
    HashMap<String,OptionValueDescription> map = new HashMap<String,OptionValueDescription>();
    for (OptionValueDescription description : result) {
      map.put(description.getName(), description);
    }

    assertOptionValue("alpha", "one", OptionPriority.COMMAND_LINE, "source",
        map.get("alpha"));
    assertOptionValue("beta", "beta", OptionPriority.DEFAULT, null,
        map.get("beta"));
    assertOptionValue("gamma", "two", OptionPriority.COMMAND_LINE, "source",
        map.get("gamma"));
    assertOptionValue("delta", "delta", OptionPriority.DEFAULT, null,
        map.get("delta"));
    assertOptionValue("echo", "echo", OptionPriority.DEFAULT, null,
        map.get("echo"));
  }

  // Regression tests for bug:
  // "--option from blazerc unexpectedly overrides --option from command line"
  public static class ListExample extends OptionsBase {
    @Option(name = "alpha",
            converter = StringConverter.class,
            allowMultiple = true,
            defaultValue = "null")
    public List<String> alpha;
  }

  @Test
  public void overrideListOptions() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(ListExample.class);
    parser.parse(OptionPriority.COMMAND_LINE, "a", Arrays.asList("--alpha=two"));
    parser.parse(OptionPriority.RC_FILE, "b", Arrays.asList("--alpha=one"));
    assertEquals(Arrays.asList("one", "two"), parser.getOptions(ListExample.class).alpha);
  }

  public static class CommaSeparatedOptionsExample extends OptionsBase {
    @Option(name = "alpha",
            converter = CommaSeparatedOptionListConverter.class,
            allowMultiple = true,
            defaultValue = "null")
    public List<String> alpha;
  }

  @Test
  public void commaSeparatedOptionsWithAllowMultiple() throws Exception {
    OptionsParser parser = OptionsParser.newOptionsParser(CommaSeparatedOptionsExample.class);
    parser.parse(OptionPriority.COMMAND_LINE, "a", Arrays.asList("--alpha=one",
        "--alpha=two,three"));
    assertEquals(Arrays.asList("one", "two", "three"),
        parser.getOptions(CommaSeparatedOptionsExample.class).alpha);
  }

  public static class IllegalListTypeExample extends OptionsBase {
    @Option(name = "alpha",
            converter = CommaSeparatedOptionListConverter.class,
            allowMultiple = true,
            defaultValue = "null")
    public List<Integer> alpha;
  }

  @Test
  public void illegalListType() throws Exception {
    try {
      OptionsParser.newOptionsParser(IllegalListTypeExample.class);
    } catch (AssertionError e) {
      // Expected exception
      return;
    }
    fail();
  }

  public static class Yesterday extends OptionsBase {

    @Option(name = "a",
            defaultValue = "a")
    public String a;

    @Option(name = "b",
            defaultValue = "b")
    public String b;

    @Option(name = "c",
            defaultValue = "null",
            expansion = {"--a=0"})
    public Void c;

    @Option(name = "d",
            defaultValue = "null",
            allowMultiple = true)
    public List<String> d;

    @Option(name = "e",
            defaultValue = "null",
            implicitRequirements = { "--a==1" })
    public String e;

    @Option(name = "f",
            defaultValue = "null",
            implicitRequirements = { "--b==1" })
    public String f;

    @Option(name = "g",
            abbrev = 'h',
            defaultValue = "false")
    public boolean g;
  }

  public static List<String> canonicalize(Class<? extends OptionsBase> optionsClass, String... args)
      throws OptionsParsingException {

    OptionsParser parser = OptionsParser.newOptionsParser(
        ImmutableList.<Class<? extends OptionsBase>>of(optionsClass));
    parser.setAllowResidue(false);
    parser.parse(Arrays.asList(args));
    return parser.canonicalize();
  }

  @Test
  public void canonicalizeEasy() throws Exception {
    assertEquals(Arrays.asList("--a=x"), canonicalize(Yesterday.class, "--a=x"));
  }

  @Test
  public void canonicalizeSkipDuplicate() throws Exception {
    assertEquals(Arrays.asList("--a=x"), canonicalize(Yesterday.class, "--a=y", "--a=x"));
  }

  @Test
  public void canonicalizeExpands() throws Exception {
    assertEquals(Arrays.asList("--a=0"), canonicalize(Yesterday.class, "--c"));
  }

  @Test
  public void canonicalizeExpansionOverridesExplicit() throws Exception {
    assertEquals(Arrays.asList("--a=0"), canonicalize(Yesterday.class, "--a=x", "--c"));
  }

  @Test
  public void canonicalizeExplicitOverridesExpansion() throws Exception {
    assertEquals(Arrays.asList("--a=x"), canonicalize(Yesterday.class, "--c", "--a=x"));
  }

  @Test
  public void canonicalizeSorts() throws Exception {
    assertEquals(Arrays.asList("--a=x", "--b=y"), canonicalize(Yesterday.class, "--b=y", "--a=x"));
  }

  @Test
  public void canonicalizeImplicitDepsAtEnd() throws Exception {
    assertEquals(Arrays.asList("--a=x", "--e=y"), canonicalize(Yesterday.class, "--e=y", "--a=x"));
  }

  @Test
  public void canonicalizeImplicitDepsSkipsDuplicate() throws Exception {
    assertEquals(Arrays.asList("--e=y"), canonicalize(Yesterday.class, "--e=x", "--e=y"));
  }

  @Test
  public void canonicalizeDoesNotSortImplicitDeps() throws Exception {
    assertEquals(Arrays.asList("--a=x", "--f=z", "--e=y"),
        canonicalize(Yesterday.class, "--f=z", "--e=y", "--a=x"));
  }

  @Test
  public void canonicalizeDoesNotSkipAllowMultiple() throws Exception {
    assertEquals(Arrays.asList("--d=a", "--d=b"),
        canonicalize(Yesterday.class, "--d=a", "--d=b"));
  }

  @Test
  public void canonicalizeReplacesAbbrevWithName() throws Exception {
    assertEquals(Arrays.asList("--g=1"),
        canonicalize(Yesterday.class, "-h"));
  }

  public static class LongValueExample extends OptionsBase {
    @Option(name = "longval",
            defaultValue = "2147483648")
    public long longval;

    @Option(name = "intval",
            defaultValue = "2147483647")
    public int intval;
  }

  @Test
  public void parseLong() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(LongValueExample.class);
    parser.parse("");
    LongValueExample result = parser.getOptions(LongValueExample.class);
    assertEquals(2147483648L, result.longval);
    assertEquals(2147483647, result.intval);

    parser.parse("--longval", Long.toString(Long.MIN_VALUE));
    result = parser.getOptions(LongValueExample.class);
    assertEquals(Long.MIN_VALUE, result.longval);

    try {
      parser.parse("--intval=2147483648");
      fail();
    } catch (OptionsParsingException e) {
    }

    parser.parse("--longval", "100");
    result = parser.getOptions(LongValueExample.class);
    assertEquals(100, result.longval);
  }

  public static class OldNameExample extends OptionsBase {
    @Option(name = "new_name",
            oldName = "old_name",
            defaultValue = "defaultValue")
    public String flag;
  }

  @Test
  public void testOldName() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(OldNameExample.class);
    parser.parse("--old_name=foo");
    OldNameExample result = parser.getOptions(OldNameExample.class);
    assertEquals("foo", result.flag);

    // Should also work by its new name.
    parser = newOptionsParser(OldNameExample.class);
    parser.parse("--new_name=foo");
    result = parser.getOptions(OldNameExample.class);
    assertEquals("foo", result.flag);
    // Should be no warnings if the new name is used.
    assertThat(parser.getWarnings()).isEmpty();
  }

  @Test
  public void testOldNameCanonicalization() throws Exception {
    assertEquals(
        Arrays.asList("--new_name=foo"), canonicalize(OldNameExample.class, "--old_name=foo"));
  }

  public static class OldNameConflictExample extends OptionsBase {
    @Option(name = "new_name",
            oldName = "old_name",
            defaultValue = "defaultValue")
    public String flag1;

    @Option(name = "old_name",
            defaultValue = "defaultValue")
    public String flag2;
  }

  @Test
  public void testOldNameConflict() {
    try {
      newOptionsParser(OldNameConflictExample.class);
      fail("old_name should conflict with the flag already named old_name");
    } catch (DuplicateOptionDeclarationException e) {
      // expected
    }
  }

  public static class WrapperOptionExample extends OptionsBase {
    @Option(name = "wrapper",
            defaultValue = "null",
            wrapperOption = true)
    public Void wrapperOption;

    @Option(name = "flag1", defaultValue = "false")
    public boolean flag1;

    @Option(name = "flag2", defaultValue = "42")
    public int flag2;

    @Option(name = "flag3", defaultValue = "foo")
    public String flag3;
  }

  @Test
  public void testWrapperOption() throws OptionsParsingException {
    OptionsParser parser = newOptionsParser(WrapperOptionExample.class);
    parser.parse("--wrapper=--flag1=true", "--wrapper=--flag2=87", "--wrapper=--flag3=bar");
    WrapperOptionExample result = parser.getOptions(WrapperOptionExample.class);
    assertEquals(true, result.flag1);
    assertEquals(87, result.flag2);
    assertEquals("bar", result.flag3);
  }

  @Test
  public void testInvalidWrapperOptionFormat() {
    OptionsParser parser = newOptionsParser(WrapperOptionExample.class);
    try {
      parser.parse("--wrapper=foo");
      fail();
    } catch (OptionsParsingException e) {
      // Check that the message looks like it's suggesting the correct format.
      assertThat(e.getMessage()).contains("--foo");
    }
  }

  @Test
  public void testWrapperCanonicalization() throws OptionsParsingException {
    List<String> canonicalized = canonicalize(WrapperOptionExample.class,
        "--wrapper=--flag1=true", "--wrapper=--flag2=87", "--wrapper=--flag3=bar");
    assertEquals(Arrays.asList("--flag1=true", "--flag2=87", "--flag3=bar"), canonicalized);
  }
}
