// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC 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.openqa.selenium.grid.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.beust.jcommander.Parameter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import org.junit.jupiter.api.Test;

class AnnotatedConfigTest {

  @Test
  void shouldAllowConfigsToBeAnnotated() {

    class WithAnnotations {

      @ConfigValue(section = "cheese", name = "type", example = "\"cheddar\"")
      private final String cheese = "brie";
    }

    WithAnnotations obj = new WithAnnotations();
    Config config = new AnnotatedConfig(obj);
    assertEquals(Optional.of("brie"), config.get("cheese", "type"));
  }

  @Test
  void shouldAllowFieldsToBeSomethingOtherThanStrings() {
    class WithTypes {

      @ConfigValue(section = "types", name = "bool", example = "false")
      private final boolean boolField = true;

      @ConfigValue(section = "types", name = "int", example = "0")
      private final int intField = 42;
    }

    Config config = new AnnotatedConfig(new WithTypes());
    assertEquals(Optional.of(true), config.getBool("types", "bool"));
    assertEquals(Optional.of(42), config.getInt("types", "int"));
  }

  @Test
  void shouldAllowCollectionTypeFieldsToBeAnnotated() {
    class WithBadAnnotation {

      @ConfigValue(section = "the", name = "collection", example = "[]")
      private final Set<String> cheeses = ImmutableSet.of("cheddar", "gouda");
    }

    AnnotatedConfig config = new AnnotatedConfig(new WithBadAnnotation());
    List<String> values =
        config
            .getAll("the", "collection")
            .orElseThrow(() -> new AssertionError("No value returned"));

    assertEquals(2, values.size());
    assertTrue(values.contains("cheddar"));
    assertTrue(values.contains("gouda"));
  }

  @Test
  void shouldNotAllowMapTypeFieldsToBeAnnotated() {
    assertThrows(
        ConfigException.class,
        () -> {
          class WithBadAnnotation {

            @ConfigValue(section = "bad", name = "map", example = "")
            private final Map<String, String> cheeses = ImmutableMap.of("peas", "sausage");
          }

          new AnnotatedConfig(new WithBadAnnotation());
        });
  }

  @Test
  void shouldWalkInheritanceHierarchy() {
    class Parent {

      @ConfigValue(section = "cheese", name = "type", example = "")
      private final String value = "cheddar";
    }

    class Child extends Parent {}

    Config config = new AnnotatedConfig(new Child());

    assertEquals(Optional.of("cheddar"), config.get("cheese", "type"));
  }

  @Test
  void configValuesFromChildClassesAreMoreImportant() {
    class Parent {

      @ConfigValue(section = "cheese", name = "type", example = "\"gouda\"")
      private final String value = "cheddar";
    }

    class Child extends Parent {

      @ConfigValue(section = "cheese", name = "type", example = "\"gouda\"")
      private final String cheese = "gorgonzola";
    }

    Config config = new AnnotatedConfig(new Child());

    assertEquals(Optional.of("gorgonzola"), config.get("cheese", "type"));
  }

  @Test
  void defaultValuesForPrimitivesAreIgnored() {
    // There's no way to tell the difference between the default values and the value having been
    // set to the default. Best not worry about it.
    class Defaults {

      // We leave booleans out --- in order to allow us to differentiate
      // "default value" from "user set value", we need to use `Boolean`
      // instead.
      @ConfigValue(section = "default", name = "bool", example = "")
      private boolean bool;

      @ConfigValue(section = "default", name = "int", example = "")
      private int integer;

      @ConfigValue(section = "default", name = "string", example = "")
      private String string;
    }

    Config config = new AnnotatedConfig(new Defaults());

    assertTrue(config.get("default", "bool").isPresent());
    assertTrue(config.getBool("default", "bool").isPresent());
    assertFalse(config.get("default", "int").isPresent());
    assertFalse(config.getInt("default", "int").isPresent());
    assertFalse(config.get("default", "string").isPresent());
  }

  @Test
  void shouldUseSetToFilterFields() {
    class TypesToBeFiltered {

      @Parameter(names = {"--bool"})
      @ConfigValue(section = "types", name = "boolean", example = "false")
      private final boolean boolField = true;

      @Parameter(names = {"--string"})
      @ConfigValue(section = "types", name = "string", example = "N/A")
      private final String stringField = "A String";

      @Parameter(names = {"--int"})
      @ConfigValue(section = "types", name = "integer", example = "0")
      private final int intField = 42;
    }

    Config config =
        new AnnotatedConfig(new TypesToBeFiltered(), ImmutableSet.of("--string", "--bool"), true);
    assertEquals(Optional.of(true), config.getBool("types", "boolean"));
    assertEquals(Optional.of("A String"), config.get("types", "string"));
    assertEquals(Optional.empty(), config.getInt("types", "integer"));
  }
}
