/*
 * 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.core;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;

import org.apache.beam.sdk.coders.BigEndianIntegerCoder;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.coders.CoderRegistry;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarIntCoder;
import org.apache.beam.sdk.transforms.Combine;
import org.apache.beam.sdk.transforms.Max;
import org.apache.beam.sdk.transforms.Min;
import org.apache.beam.sdk.transforms.windowing.TimestampCombiner;
import org.apache.beam.sdk.util.CombineFnUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link StateTag}. */
@RunWith(JUnit4.class)
public class StateTagTest {
  @Test
  public void testValueEquality() {
    StateTag<?> fooVarInt1 = StateTags.value("foo", VarIntCoder.of());
    StateTag<?> fooVarInt2 = StateTags.value("foo", VarIntCoder.of());
    StateTag<?> fooBigEndian = StateTags.value("foo", BigEndianIntegerCoder.of());
    StateTag<?> barVarInt = StateTags.value("bar", VarIntCoder.of());

    assertEquals(fooVarInt1, fooVarInt2);
    assertNotEquals(fooVarInt1, fooBigEndian);
    assertNotEquals(fooVarInt1, barVarInt);
  }

  @Test
  public void testBagEquality() {
    StateTag<?> fooVarInt1 = StateTags.bag("foo", VarIntCoder.of());
    StateTag<?> fooVarInt2 = StateTags.bag("foo", VarIntCoder.of());
    StateTag<?> fooBigEndian = StateTags.bag("foo", BigEndianIntegerCoder.of());
    StateTag<?> barVarInt = StateTags.bag("bar", VarIntCoder.of());

    assertEquals(fooVarInt1, fooVarInt2);
    assertNotEquals(fooVarInt1, fooBigEndian);
    assertNotEquals(fooVarInt1, barVarInt);
  }

  @Test
  public void testSetEquality() {
    StateTag<?> fooVarInt1 = StateTags.set("foo", VarIntCoder.of());
    StateTag<?> fooVarInt2 = StateTags.set("foo", VarIntCoder.of());
    StateTag<?> fooBigEndian = StateTags.set("foo", BigEndianIntegerCoder.of());
    StateTag<?> barVarInt = StateTags.set("bar", VarIntCoder.of());

    assertEquals(fooVarInt1, fooVarInt2);
    assertNotEquals(fooVarInt1, fooBigEndian);
    assertNotEquals(fooVarInt1, barVarInt);
  }

  @Test
  public void testMapEquality() {
    StateTag<?> fooStringVarInt1 = StateTags.map("foo", StringUtf8Coder.of(), VarIntCoder.of());
    StateTag<?> fooStringVarInt2 = StateTags.map("foo", StringUtf8Coder.of(), VarIntCoder.of());
    StateTag<?> fooStringBigEndian =
        StateTags.map("foo", StringUtf8Coder.of(), BigEndianIntegerCoder.of());
    StateTag<?> fooVarIntBigEndian =
        StateTags.map("foo", VarIntCoder.of(), BigEndianIntegerCoder.of());
    StateTag<?> barStringVarInt = StateTags.map("bar", StringUtf8Coder.of(), VarIntCoder.of());

    assertEquals(fooStringVarInt1, fooStringVarInt2);
    assertNotEquals(fooStringVarInt1, fooStringBigEndian);
    assertNotEquals(fooStringBigEndian, fooVarIntBigEndian);
    assertNotEquals(fooStringVarInt1, fooVarIntBigEndian);
    assertNotEquals(fooStringVarInt1, barStringVarInt);
  }

  @Test
  public void testWatermarkBagEquality() {
    StateTag<?> foo1 = StateTags.watermarkStateInternal("foo", TimestampCombiner.EARLIEST);
    StateTag<?> foo2 = StateTags.watermarkStateInternal("foo", TimestampCombiner.EARLIEST);
    StateTag<?> bar = StateTags.watermarkStateInternal("bar", TimestampCombiner.EARLIEST);

    StateTag<?> bar2 = StateTags.watermarkStateInternal("bar", TimestampCombiner.LATEST);

    // Same id, same fn.
    assertEquals(foo1, foo2);
    // Different id, same fn.
    assertNotEquals(foo1, bar);
    // Same id, different fn.
    assertEquals(bar, bar2);
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void testCombiningValueEquality() {
    Combine.BinaryCombineIntegerFn maxFn = Max.ofIntegers();
    Coder<Integer> input1 = VarIntCoder.of();
    Coder<Integer> input2 = BigEndianIntegerCoder.of();
    Combine.BinaryCombineIntegerFn minFn = Min.ofIntegers();

    StateTag<?> fooCoder1Max1 = StateTags.combiningValueFromInputInternal("foo", input1, maxFn);
    StateTag<?> fooCoder1Max2 = StateTags.combiningValueFromInputInternal("foo", input1, maxFn);
    StateTag<?> fooCoder1Min = StateTags.combiningValueFromInputInternal("foo", input1, minFn);

    StateTag<?> fooCoder2Max = StateTags.combiningValueFromInputInternal("foo", input2, maxFn);
    StateTag<?> barCoder1Max = StateTags.combiningValueFromInputInternal("bar", input1, maxFn);

    // Same name, coder and combineFn
    assertEquals(fooCoder1Max1, fooCoder1Max2);
    assertEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max2));

    // Different combineFn, but we treat them as equal since we only serialize the bits.
    assertEquals(fooCoder1Max1, fooCoder1Min);
    assertEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Min));

    // Different input coder coder.
    assertNotEquals(fooCoder1Max1, fooCoder2Max);
    assertNotEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder2Max));

    // These StateTags have different IDs.
    assertNotEquals(fooCoder1Max1, barCoder1Max);
    assertNotEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) barCoder1Max));
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  @Test
  public void testCombiningValueWithContextEquality() {
    CoderRegistry registry = CoderRegistry.createDefault();

    Combine.BinaryCombineIntegerFn maxFn = Max.ofIntegers();
    Combine.BinaryCombineIntegerFn minFn = Min.ofIntegers();

    Coder<int[]> accum1 = maxFn.getAccumulatorCoder(registry, VarIntCoder.of());
    Coder<int[]> accum2 = minFn.getAccumulatorCoder(registry, BigEndianIntegerCoder.of());

    StateTag<?> fooCoder1Max1 =
        StateTags.combiningValueWithContext("foo", accum1, CombineFnUtil.toFnWithContext(maxFn));
    StateTag<?> fooCoder1Max2 =
        StateTags.combiningValueWithContext("foo", accum1, CombineFnUtil.toFnWithContext(maxFn));
    StateTag<?> fooCoder1Min =
        StateTags.combiningValueWithContext("foo", accum1, CombineFnUtil.toFnWithContext(minFn));

    StateTag<?> fooCoder2Max =
        StateTags.combiningValueWithContext("foo", accum2, CombineFnUtil.toFnWithContext(maxFn));
    StateTag<?> barCoder1Max =
        StateTags.combiningValueWithContext("bar", accum1, CombineFnUtil.toFnWithContext(maxFn));

    // Same name, coder and combineFn
    assertEquals(fooCoder1Max1, fooCoder1Max2);
    assertEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max2));
    // Different combineFn, but we treat them as equal since we only serialize the bits.
    assertEquals(fooCoder1Max1, fooCoder1Min);
    assertEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Min));

    // Different input coder coder.
    assertNotEquals(fooCoder1Max1, fooCoder2Max);
    assertNotEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) fooCoder2Max));

    // These StateTags have different IDs.
    assertNotEquals(fooCoder1Max1, barCoder1Max);
    assertNotEquals(
        StateTags.convertToBagTagInternal((StateTag) fooCoder1Max1),
        StateTags.convertToBagTagInternal((StateTag) barCoder1Max));
  }
}
