/*
 * 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.runners.dataflow.util;

import static org.apache.beam.runners.dataflow.util.Structs.addBoolean;
import static org.apache.beam.runners.dataflow.util.Structs.addDouble;
import static org.apache.beam.runners.dataflow.util.Structs.addList;
import static org.apache.beam.runners.dataflow.util.Structs.addLong;
import static org.apache.beam.runners.dataflow.util.Structs.addLongs;
import static org.apache.beam.runners.dataflow.util.Structs.addNull;
import static org.apache.beam.runners.dataflow.util.Structs.addString;
import static org.apache.beam.runners.dataflow.util.Structs.addStringList;
import static org.apache.beam.runners.dataflow.util.Structs.getBoolean;
import static org.apache.beam.runners.dataflow.util.Structs.getDictionary;
import static org.apache.beam.runners.dataflow.util.Structs.getInt;
import static org.apache.beam.runners.dataflow.util.Structs.getListOfMaps;
import static org.apache.beam.runners.dataflow.util.Structs.getLong;
import static org.apache.beam.runners.dataflow.util.Structs.getObject;
import static org.apache.beam.runners.dataflow.util.Structs.getString;
import static org.apache.beam.runners.dataflow.util.Structs.getStrings;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for Structs. */
@RunWith(JUnit4.class)
@SuppressWarnings({
  "nullness" // TODO(https://issues.apache.org/jira/browse/BEAM-10402)
})
public class StructsTest {
  private List<Map<String, Object>> makeCloudObjects() {
    List<Map<String, Object>> objects = new ArrayList<>();
    {
      CloudObject o = CloudObject.forClassName("string");
      addString(o, "singletonStringKey", "stringValue");
      objects.add(o);
    }
    {
      CloudObject o = CloudObject.forClassName("long");
      addLong(o, "singletonLongKey", 42L);
      objects.add(o);
    }
    return objects;
  }

  private Map<String, Object> makeCloudDictionary() {
    Map<String, Object> o = new HashMap<>();
    addList(o, "emptyKey", Collections.emptyList());
    addNull(o, "noStringsKey");
    addString(o, "singletonStringKey", "stringValue");
    addStringList(o, "multipleStringsKey", Arrays.asList("hi", "there", "bob"));
    addLongs(o, "multipleLongsKey", 47L, 1L << 42, -5L);
    addLong(o, "singletonLongKey", 42L);
    addDouble(o, "singletonDoubleKey", Math.PI);
    addBoolean(o, "singletonBooleanKey", true);
    addNull(o, "noObjectsKey");
    addList(o, "multipleObjectsKey", makeCloudObjects());
    return o;
  }

  @Test
  public void testGetStringParameter() throws Exception {
    Map<String, Object> o = makeCloudDictionary();

    Assert.assertEquals("stringValue", getString(o, "singletonStringKey"));
    Assert.assertEquals("stringValue", getString(o, "singletonStringKey", "defaultValue"));
    Assert.assertEquals("defaultValue", getString(o, "missingKey", "defaultValue"));

    try {
      getString(o, "missingKey");
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(
          exn.toString(), Matchers.containsString("didn't find required parameter missingKey"));
    }

    try {
      getString(o, "noStringsKey");
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a string"));
    }

    Assert.assertThat(getStrings(o, "noStringsKey", null), Matchers.emptyIterable());
    Assert.assertThat(getObject(o, "noStringsKey").keySet(), Matchers.emptyIterable());
    Assert.assertThat(getDictionary(o, "noStringsKey").keySet(), Matchers.emptyIterable());
    Assert.assertThat(getDictionary(o, "noStringsKey", null).keySet(), Matchers.emptyIterable());

    try {
      getString(o, "multipleStringsKey");
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a string"));
    }

    try {
      getString(o, "emptyKey");
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a string"));
    }
  }

  @Test
  public void testGetBooleanParameter() throws Exception {
    Map<String, Object> o = makeCloudDictionary();

    Assert.assertTrue(getBoolean(o, "singletonBooleanKey", false));
    Assert.assertFalse(getBoolean(o, "missingKey", false));

    try {
      getBoolean(o, "emptyKey", false);
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a boolean"));
    }
  }

  @Test
  public void testGetLongParameter() throws Exception {
    Map<String, Object> o = makeCloudDictionary();

    Assert.assertEquals((Long) 42L, getLong(o, "singletonLongKey", 666L));
    Assert.assertEquals((Integer) 42, getInt(o, "singletonLongKey", 666));
    Assert.assertEquals((Long) 666L, getLong(o, "missingKey", 666L));

    try {
      getLong(o, "emptyKey", 666L);
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a long"));
    }
    try {
      getInt(o, "emptyKey", 666);
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not an int"));
    }
  }

  @Test
  public void testGetListOfMaps() throws Exception {
    Map<String, Object> o = makeCloudDictionary();

    Assert.assertEquals(makeCloudObjects(), getListOfMaps(o, "multipleObjectsKey", null));

    try {
      getListOfMaps(o, "singletonLongKey", null);
      Assert.fail("should have thrown an exception");
    } catch (Exception exn) {
      Assert.assertThat(exn.toString(), Matchers.containsString("not a list"));
    }
  }

  // TODO: Test builder operations.
}
