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

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.emptyIterable;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.lessThan;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.when;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.apache.beam.runners.core.StateNamespaces;
import org.apache.beam.runners.core.TimerInternals.TimerData;
import org.apache.beam.runners.direct.WatermarkManager.AppliedPTransformInputWatermark;
import org.apache.beam.runners.direct.WatermarkManager.FiredTimers;
import org.apache.beam.runners.direct.WatermarkManager.TimerUpdate;
import org.apache.beam.runners.direct.WatermarkManager.TimerUpdate.TimerUpdateBuilder;
import org.apache.beam.runners.direct.WatermarkManager.TransformWatermarks;
import org.apache.beam.runners.direct.WatermarkManager.Watermark;
import org.apache.beam.runners.local.StructuralKey;
import org.apache.beam.sdk.coders.ByteArrayCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarLongCoder;
import org.apache.beam.sdk.runners.AppliedPTransform;
import org.apache.beam.sdk.state.TimeDomain;
import org.apache.beam.sdk.testing.TestPipeline;
import org.apache.beam.sdk.transforms.Create;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.Filter;
import org.apache.beam.sdk.transforms.Flatten;
import org.apache.beam.sdk.transforms.ParDo;
import org.apache.beam.sdk.transforms.WithKeys;
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
import org.apache.beam.sdk.transforms.windowing.GlobalWindow;
import org.apache.beam.sdk.transforms.windowing.IntervalWindow;
import org.apache.beam.sdk.transforms.windowing.PaneInfo;
import org.apache.beam.sdk.util.WindowedValue;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.PCollectionList;
import org.apache.beam.sdk.values.TimestampedValue;
import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.ImmutableList;
import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.ImmutableMap;
import org.apache.beam.vendor.guava.v20_0.com.google.common.collect.Iterables;
import org.joda.time.Duration;
import org.joda.time.Instant;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mockito;

/** Tests for {@link WatermarkManager}. */
@RunWith(JUnit4.class)
public class WatermarkManagerTest implements Serializable {
  @Rule public transient ExpectedException thrown = ExpectedException.none();

  private transient MockClock clock;

  private transient PCollection<Integer> createdInts;

  private transient PCollection<Integer> filtered;
  private transient PCollection<Integer> filteredTimesTwo;
  private transient PCollection<KV<String, Integer>> keyed;

  private transient PCollection<Integer> intsToFlatten;
  private transient PCollection<Integer> flattened;

  private transient WatermarkManager<AppliedPTransform<?, ?, ?>, ? super PCollection<?>> manager;
  private transient BundleFactory bundleFactory;
  private DirectGraph graph;

  @Rule
  public transient TestPipeline p = TestPipeline.create().enableAbandonedNodeEnforcement(false);

  @Before
  public void setup() {

    createdInts = p.apply("createdInts", Create.of(1, 2, 3));

    filtered = createdInts.apply("filtered", Filter.greaterThan(1));
    filteredTimesTwo =
        filtered.apply(
            "timesTwo",
            ParDo.of(
                new DoFn<Integer, Integer>() {
                  @ProcessElement
                  public void processElement(ProcessContext c) throws Exception {
                    c.output(c.element() * 2);
                  }
                }));

    keyed = createdInts.apply("keyed", WithKeys.of("MyKey"));

    intsToFlatten = p.apply("intsToFlatten", Create.of(-1, 256, 65535));
    PCollectionList<Integer> preFlatten = PCollectionList.of(createdInts).and(intsToFlatten);
    flattened = preFlatten.apply("flattened", Flatten.pCollections());

    clock = MockClock.fromInstant(new Instant(1000));
    DirectGraphs.performDirectOverrides(p);
    graph = DirectGraphs.getGraph(p);

    manager = WatermarkManager.create(clock, graph, AppliedPTransform::getFullName);
    bundleFactory = ImmutableListBundleFactory.create();
  }

  /**
   * Demonstrates that getWatermark, when called on an {@link AppliedPTransform} that has not
   * processed any elements, returns the {@link BoundedWindow#TIMESTAMP_MIN_VALUE}.
   */
  @Test
  public void getWatermarkForUntouchedTransform() {
    TransformWatermarks watermarks = manager.getWatermarks(graph.getProducer(createdInts));

    assertThat(watermarks.getInputWatermark(), equalTo(BoundedWindow.TIMESTAMP_MIN_VALUE));
    assertThat(watermarks.getOutputWatermark(), equalTo(BoundedWindow.TIMESTAMP_MIN_VALUE));
  }

  /**
   * Demonstrates that getWatermark for a transform that consumes no input uses the Watermark Hold
   * value provided to it as the output watermark.
   */
  @Test
  public void getWatermarkForUpdatedSourceTransform() {
    CommittedBundle<Integer> output = multiWindowedBundle(createdInts, 1);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.singleton(output),
        new Instant(8000L));
    manager.refreshAll();
    TransformWatermarks updatedSourceWatermark =
        manager.getWatermarks(graph.getProducer(createdInts));

    assertThat(updatedSourceWatermark.getOutputWatermark(), equalTo(new Instant(8000L)));
  }

  /**
   * Demonstrates that getWatermark for a transform that takes multiple inputs is held to the
   * minimum watermark across all of its inputs.
   */
  @Test
  public void getWatermarkForMultiInputTransform() {
    CommittedBundle<Integer> secondPcollectionBundle = multiWindowedBundle(intsToFlatten, -1);

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(intsToFlatten),
        null,
        Collections.<CommittedBundle<?>>singleton(secondPcollectionBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    // We didn't do anything for the first source, so we shouldn't have progressed the watermark
    TransformWatermarks firstSourceWatermark =
        manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(
        firstSourceWatermark.getOutputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));

    // the Second Source output all of the elements so it should be done (with a watermark at the
    // end of time).
    TransformWatermarks secondSourceWatermark =
        manager.getWatermarks(graph.getProducer(intsToFlatten));
    assertThat(
        secondSourceWatermark.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    // We haven't consumed anything yet, so our watermark should be at the beginning of time
    TransformWatermarks transformWatermark = manager.getWatermarks(graph.getProducer(flattened));
    assertThat(
        transformWatermark.getInputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));
    assertThat(
        transformWatermark.getOutputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));

    CommittedBundle<Integer> flattenedBundleSecondCreate = multiWindowedBundle(flattened, -1);
    // We have finished processing the bundle from the second PCollection, but we haven't consumed
    // anything from the first PCollection yet; so our watermark shouldn't advance
    manager.updateWatermarks(
        secondPcollectionBundle,
        TimerUpdate.empty(),
        graph.getProducer(flattened),
        secondPcollectionBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(flattenedBundleSecondCreate),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    TransformWatermarks transformAfterProcessing =
        manager.getWatermarks(graph.getProducer(flattened));
    manager.updateWatermarks(
        secondPcollectionBundle,
        TimerUpdate.empty(),
        graph.getProducer(flattened),
        secondPcollectionBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(flattenedBundleSecondCreate),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    assertThat(
        transformAfterProcessing.getInputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));
    assertThat(
        transformAfterProcessing.getOutputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));

    Instant firstCollectionTimestamp = new Instant(10000);
    CommittedBundle<Integer> firstPcollectionBundle =
        timestampedBundle(createdInts, TimestampedValue.of(5, firstCollectionTimestamp));
    // the source is done, but elements are still buffered. The source output watermark should be
    // past the end of the global window
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(firstPcollectionBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks firstSourceWatermarks =
        manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(
        firstSourceWatermarks.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    // We still haven't consumed any of the first source's input, so the watermark should still not
    // progress
    TransformWatermarks flattenAfterSourcesProduced =
        manager.getWatermarks(graph.getProducer(flattened));
    assertThat(
        flattenAfterSourcesProduced.getInputWatermark(),
        not(greaterThan(firstCollectionTimestamp)));
    assertThat(
        flattenAfterSourcesProduced.getOutputWatermark(),
        not(greaterThan(firstCollectionTimestamp)));

    // We have buffered inputs, but since the PCollection has all of the elements (has a WM past the
    // end of the global window), we should have a watermark equal to the min among buffered
    // elements
    TransformWatermarks withBufferedElements = manager.getWatermarks(graph.getProducer(flattened));
    assertThat(withBufferedElements.getInputWatermark(), equalTo(firstCollectionTimestamp));
    assertThat(withBufferedElements.getOutputWatermark(), equalTo(firstCollectionTimestamp));

    CommittedBundle<?> completedFlattenBundle =
        bundleFactory.createBundle(flattened).commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.updateWatermarks(
        firstPcollectionBundle,
        TimerUpdate.empty(),
        graph.getProducer(flattened),
        firstPcollectionBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(completedFlattenBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks afterConsumingAllInput =
        manager.getWatermarks(graph.getProducer(flattened));
    assertThat(
        afterConsumingAllInput.getInputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(
        afterConsumingAllInput.getOutputWatermark(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
  }

  /**
   * Demonstrates that getWatermark for a transform that takes multiple inputs is held to the
   * minimum watermark across all of its inputs.
   */
  @Test
  public void getWatermarkMultiIdenticalInput() {
    PCollection<Integer> created = p.apply(Create.of(1, 2, 3));
    PCollection<Integer> multiConsumer =
        PCollectionList.of(created).and(created).apply(Flatten.pCollections());
    DirectGraphVisitor graphVisitor = new DirectGraphVisitor();
    p.traverseTopologically(graphVisitor);
    DirectGraph graph = graphVisitor.getGraph();

    AppliedPTransform<?, ?, ?> theFlatten = graph.getProducer(multiConsumer);

    WatermarkManager<AppliedPTransform<?, ?, ?>, ? super PCollection<?>> tstMgr =
        WatermarkManager.create(clock, graph, AppliedPTransform::getFullName);
    CommittedBundle<Void> root =
        bundleFactory
            .<Void>createRootBundle()
            .add(WindowedValue.valueInGlobalWindow(null))
            .commit(clock.now());
    CommittedBundle<Integer> createBundle =
        bundleFactory
            .createBundle(created)
            .add(WindowedValue.timestampedValueInGlobalWindow(1, new Instant(33536)))
            .commit(clock.now());

    Map<AppliedPTransform<?, ?, ?>, Collection<CommittedBundle<?>>> initialInputs =
        ImmutableMap.<AppliedPTransform<?, ?, ?>, Collection<CommittedBundle<?>>>builder()
            .put(graph.getProducer(created), Collections.singleton(root))
            .build();
    tstMgr.initialize((Map) initialInputs);
    tstMgr.updateWatermarks(
        root,
        TimerUpdate.empty(),
        graph.getProducer(created),
        null,
        Collections.singleton(createBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    tstMgr.refreshAll();
    TransformWatermarks flattenWms = tstMgr.getWatermarks(theFlatten);
    assertThat(flattenWms.getInputWatermark(), equalTo(new Instant(33536)));

    tstMgr.updateWatermarks(
        createBundle,
        TimerUpdate.empty(),
        theFlatten,
        null,
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    tstMgr.refreshAll();
    assertThat(flattenWms.getInputWatermark(), equalTo(new Instant(33536)));

    tstMgr.updateWatermarks(
        createBundle,
        TimerUpdate.empty(),
        theFlatten,
        null,
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    tstMgr.refreshAll();
    assertThat(flattenWms.getInputWatermark(), equalTo(BoundedWindow.TIMESTAMP_MAX_VALUE));
  }

  /**
   * Demonstrates that pending elements are independent among {@link AppliedPTransform
   * AppliedPTransforms} that consume the same input {@link PCollection}.
   */
  @Test
  public void getWatermarkForMultiConsumedCollection() {
    CommittedBundle<Integer> createdBundle =
        timestampedBundle(
            createdInts,
            TimestampedValue.of(1, new Instant(1_000_000L)),
            TimestampedValue.of(2, new Instant(1234L)),
            TimestampedValue.of(3, new Instant(-1000L)));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        new Instant(Long.MAX_VALUE));
    manager.refreshAll();
    TransformWatermarks createdAfterProducing =
        manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(
        createdAfterProducing.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    CommittedBundle<KV<String, Integer>> keyBundle =
        timestampedBundle(
            keyed,
            TimestampedValue.of(KV.of("MyKey", 1), new Instant(1_000_000L)),
            TimestampedValue.of(KV.of("MyKey", 2), new Instant(1234L)),
            TimestampedValue.of(KV.of("MyKey", 3), new Instant(-1000L)));
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(keyBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks keyedWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(
        keyedWatermarks.getInputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(
        keyedWatermarks.getOutputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    TransformWatermarks filteredWatermarks = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(filteredWatermarks.getInputWatermark(), not(greaterThan(new Instant(-1000L))));
    assertThat(filteredWatermarks.getOutputWatermark(), not(greaterThan(new Instant(-1000L))));

    CommittedBundle<Integer> filteredBundle =
        timestampedBundle(filtered, TimestampedValue.of(2, new Instant(1234L)));
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filteredBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks filteredProcessedWatermarks =
        manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        filteredProcessedWatermarks.getInputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(
        filteredProcessedWatermarks.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
  }

  /**
   * Demonstrates that the watermark of an {@link AppliedPTransform} is held to the provided
   * watermark hold.
   */
  @Test
  public void updateWatermarkWithWatermarkHolds() {
    CommittedBundle<Integer> createdBundle =
        timestampedBundle(
            createdInts,
            TimestampedValue.of(1, new Instant(1_000_000L)),
            TimestampedValue.of(2, new Instant(1234L)),
            TimestampedValue.of(3, new Instant(-1000L)));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        new Instant(Long.MAX_VALUE));

    CommittedBundle<KV<String, Integer>> keyBundle =
        timestampedBundle(
            keyed,
            TimestampedValue.of(KV.of("MyKey", 1), new Instant(1_000_000L)),
            TimestampedValue.of(KV.of("MyKey", 2), new Instant(1234L)),
            TimestampedValue.of(KV.of("MyKey", 3), new Instant(-1000L)));
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(keyBundle),
        new Instant(500L));
    manager.refreshAll();
    TransformWatermarks keyedWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(
        keyedWatermarks.getInputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(keyedWatermarks.getOutputWatermark(), not(greaterThan(new Instant(500L))));
  }

  /**
   * Demonstrates that the watermark of an {@link AppliedPTransform} is held to the provided
   * watermark hold.
   */
  @Test
  public void updateWatermarkWithKeyedWatermarkHolds() {
    CommittedBundle<Integer> firstKeyBundle =
        bundleFactory
            .createKeyedBundle(StructuralKey.of("Odd", StringUtf8Coder.of()), createdInts)
            .add(WindowedValue.timestampedValueInGlobalWindow(1, new Instant(1_000_000L)))
            .add(WindowedValue.timestampedValueInGlobalWindow(3, new Instant(-1000L)))
            .commit(clock.now());

    CommittedBundle<Integer> secondKeyBundle =
        bundleFactory
            .createKeyedBundle(StructuralKey.of("Even", StringUtf8Coder.of()), createdInts)
            .add(WindowedValue.timestampedValueInGlobalWindow(2, new Instant(1234L)))
            .commit(clock.now());

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        ImmutableList.of(firstKeyBundle, secondKeyBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    manager.updateWatermarks(
        firstKeyBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        firstKeyBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        new Instant(-1000L));
    manager.updateWatermarks(
        secondKeyBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        secondKeyBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        new Instant(1234L));
    manager.refreshAll();

    TransformWatermarks filteredWatermarks = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        filteredWatermarks.getInputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(filteredWatermarks.getOutputWatermark(), not(greaterThan(new Instant(-1000L))));

    CommittedBundle<Integer> fauxFirstKeyTimerBundle =
        bundleFactory
            .createKeyedBundle(StructuralKey.of("Odd", StringUtf8Coder.of()), createdInts)
            .commit(clock.now());
    manager.updateWatermarks(
        fauxFirstKeyTimerBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        fauxFirstKeyTimerBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    assertThat(filteredWatermarks.getOutputWatermark(), equalTo(new Instant(1234L)));

    CommittedBundle<Integer> fauxSecondKeyTimerBundle =
        bundleFactory
            .createKeyedBundle(StructuralKey.of("Even", StringUtf8Coder.of()), createdInts)
            .commit(clock.now());
    manager.updateWatermarks(
        fauxSecondKeyTimerBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        fauxSecondKeyTimerBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        new Instant(5678L));
    manager.refreshAll();
    assertThat(filteredWatermarks.getOutputWatermark(), equalTo(new Instant(5678L)));

    manager.updateWatermarks(
        fauxSecondKeyTimerBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        fauxSecondKeyTimerBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    assertThat(
        filteredWatermarks.getOutputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
  }

  /**
   * Demonstrates that updated output watermarks are monotonic in the presence of late data, when
   * called on an {@link AppliedPTransform} that consumes no input.
   */
  @Test
  public void updateOutputWatermarkShouldBeMonotonic() {
    CommittedBundle<?> firstInput =
        bundleFactory.createBundle(createdInts).commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(firstInput),
        new Instant(0L));
    manager.refreshAll();
    TransformWatermarks firstWatermarks = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(firstWatermarks.getOutputWatermark(), equalTo(new Instant(0L)));

    CommittedBundle<?> secondInput =
        bundleFactory.createBundle(createdInts).commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(secondInput),
        new Instant(-250L));
    manager.refreshAll();
    TransformWatermarks secondWatermarks = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(secondWatermarks.getOutputWatermark(), not(lessThan(new Instant(0L))));
  }

  /**
   * Demonstrates that updated output watermarks are monotonic in the presence of watermark holds
   * that become earlier than a previous watermark hold.
   */
  @Test
  public void updateWatermarkWithHoldsShouldBeMonotonic() {
    CommittedBundle<Integer> createdBundle =
        timestampedBundle(
            createdInts,
            TimestampedValue.of(1, new Instant(1_000_000L)),
            TimestampedValue.of(2, new Instant(1234L)),
            TimestampedValue.of(3, new Instant(-1000L)));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        new Instant(Long.MAX_VALUE));

    CommittedBundle<KV<String, Integer>> keyBundle =
        timestampedBundle(
            keyed,
            TimestampedValue.of(KV.of("MyKey", 1), new Instant(1_000_000L)),
            TimestampedValue.of(KV.of("MyKey", 2), new Instant(1234L)),
            TimestampedValue.of(KV.of("MyKey", 3), new Instant(-1000L)));
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(keyBundle),
        new Instant(500L));
    manager.refreshAll();
    TransformWatermarks keyedWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(
        keyedWatermarks.getInputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(keyedWatermarks.getOutputWatermark(), not(greaterThan(new Instant(500L))));
    Instant oldOutputWatermark = keyedWatermarks.getOutputWatermark();

    TransformWatermarks updatedWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(
        updatedWatermarks.getInputWatermark(), not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    // We added a hold prior to the old watermark; we shouldn't progress (due to the earlier hold)
    // but the watermark is monotonic and should not backslide to the new, earlier hold
    assertThat(updatedWatermarks.getOutputWatermark(), equalTo(oldOutputWatermark));
  }

  @Test
  public void updateWatermarkWithUnprocessedElements() {
    WindowedValue<Integer> first = WindowedValue.valueInGlobalWindow(1);
    WindowedValue<Integer> second =
        WindowedValue.timestampedValueInGlobalWindow(2, new Instant(-1000L));
    WindowedValue<Integer> third =
        WindowedValue.timestampedValueInGlobalWindow(3, new Instant(1234L));
    CommittedBundle<Integer> createdBundle =
        bundleFactory
            .createBundle(createdInts)
            .add(first)
            .add(second)
            .add(third)
            .commit(clock.now());

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    CommittedBundle<KV<String, Integer>> keyBundle =
        timestampedBundle(
            keyed, TimestampedValue.of(KV.of("MyKey", 1), BoundedWindow.TIMESTAMP_MIN_VALUE));
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(ImmutableList.of(second, third)),
        Collections.<CommittedBundle<?>>singleton(keyBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    TransformWatermarks keyedWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    // the unprocessed second and third are readded to pending
    assertThat(keyedWatermarks.getInputWatermark(), not(greaterThan(new Instant(-1000L))));
  }

  @Test
  public void updateWatermarkWithCompletedElementsNotPending() {
    WindowedValue<Integer> first = WindowedValue.timestampedValueInGlobalWindow(1, new Instant(22));
    CommittedBundle<Integer> createdBundle =
        bundleFactory.createBundle(createdInts).add(first).commit(clock.now());

    WindowedValue<Integer> second =
        WindowedValue.timestampedValueInGlobalWindow(2, new Instant(22));
    CommittedBundle<Integer> neverCreatedBundle =
        bundleFactory.createBundle(createdInts).add(second).commit(clock.now());

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    manager.updateWatermarks(
        neverCreatedBundle,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        neverCreatedBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    manager.refreshAll();
    TransformWatermarks filteredWms = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(filteredWms.getInputWatermark(), equalTo(new Instant(22L)));
  }

  /** Demonstrates that updateWatermarks in the presence of late data is monotonic. */
  @Test
  public void updateWatermarkWithLateData() {
    Instant sourceWatermark = new Instant(1_000_000L);
    CommittedBundle<Integer> createdBundle =
        timestampedBundle(
            createdInts,
            TimestampedValue.of(1, sourceWatermark),
            TimestampedValue.of(2, new Instant(1234L)));

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        sourceWatermark);

    CommittedBundle<KV<String, Integer>> keyBundle =
        timestampedBundle(
            keyed,
            TimestampedValue.of(KV.of("MyKey", 1), sourceWatermark),
            TimestampedValue.of(KV.of("MyKey", 2), new Instant(1234L)));

    // Finish processing the on-time data. The watermarks should progress to be equal to the source
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(keyBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks onTimeWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(onTimeWatermarks.getInputWatermark(), equalTo(sourceWatermark));
    assertThat(onTimeWatermarks.getOutputWatermark(), equalTo(sourceWatermark));

    CommittedBundle<Integer> lateDataBundle =
        timestampedBundle(createdInts, TimestampedValue.of(3, new Instant(-1000L)));
    // the late data arrives in a downstream PCollection after its watermark has advanced past it;
    // we don't advance the watermark past the current watermark until we've consumed the late data
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(lateDataBundle),
        new Instant(2_000_000L));
    manager.refreshAll();
    TransformWatermarks bufferedLateWm = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(bufferedLateWm.getOutputWatermark(), equalTo(new Instant(2_000_000L)));

    // The input watermark should be held to its previous value (not advanced due to late data; not
    // moved backwards in the presence of watermarks due to monotonicity).
    TransformWatermarks lateDataBufferedWatermark = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(lateDataBufferedWatermark.getInputWatermark(), not(lessThan(sourceWatermark)));
    assertThat(lateDataBufferedWatermark.getOutputWatermark(), not(lessThan(sourceWatermark)));

    CommittedBundle<KV<String, Integer>> lateKeyedBundle =
        timestampedBundle(keyed, TimestampedValue.of(KV.of("MyKey", 3), new Instant(-1000L)));
    manager.updateWatermarks(
        lateDataBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        lateDataBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(lateKeyedBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
  }

  @Test
  @Ignore("https://issues.apache.org/jira/browse/BEAM-4191")
  public void updateWatermarkWithDifferentWindowedValueInstances() {
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(
            bundleFactory
                .createBundle(createdInts)
                .add(WindowedValue.valueInGlobalWindow(1))
                .commit(Instant.now())),
        BoundedWindow.TIMESTAMP_MAX_VALUE);

    CommittedBundle<Integer> createdBundle =
        bundleFactory
            .createBundle(createdInts)
            .add(WindowedValue.valueInGlobalWindow(1))
            .commit(Instant.now());
    manager.updateWatermarks(
        createdBundle,
        TimerUpdate.empty(),
        graph.getProducer(keyed),
        createdBundle.withElements(Collections.emptyList()),
        Collections.emptyList(),
        null);
    manager.refreshAll();
    TransformWatermarks onTimeWatermarks = manager.getWatermarks(graph.getProducer(keyed));
    assertThat(onTimeWatermarks.getInputWatermark(), equalTo(BoundedWindow.TIMESTAMP_MAX_VALUE));
  }

  /**
   * Demonstrates that after watermarks of an upstream transform are updated, but no output has been
   * produced, the watermarks of a downstream process are advanced.
   */
  @Test
  public void getWatermarksAfterOnlyEmptyOutput() {
    CommittedBundle<Integer> emptyCreateOutput = multiWindowedBundle(createdInts);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(emptyCreateOutput),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks updatedSourceWatermarks =
        manager.getWatermarks(graph.getProducer(createdInts));

    assertThat(
        updatedSourceWatermarks.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    TransformWatermarks finishedFilterWatermarks =
        manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        finishedFilterWatermarks.getInputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(
        finishedFilterWatermarks.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
  }

  /**
   * Demonstrates that after watermarks of an upstream transform are updated, but no output has been
   * produced, and the downstream transform has a watermark hold, the watermark is held to the hold.
   */
  @Test
  public void getWatermarksAfterHoldAndEmptyOutput() {
    CommittedBundle<Integer> firstCreateOutput = multiWindowedBundle(createdInts, 1, 2);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(firstCreateOutput),
        new Instant(12_000L));

    CommittedBundle<Integer> firstFilterOutput = multiWindowedBundle(filtered);
    manager.updateWatermarks(
        firstCreateOutput,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        firstCreateOutput.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(firstFilterOutput),
        new Instant(10_000L));
    manager.refreshAll();
    TransformWatermarks firstFilterWatermarks = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(firstFilterWatermarks.getInputWatermark(), not(lessThan(new Instant(12_000L))));
    assertThat(firstFilterWatermarks.getOutputWatermark(), not(greaterThan(new Instant(10_000L))));

    CommittedBundle<Integer> emptyCreateOutput = multiWindowedBundle(createdInts);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(emptyCreateOutput),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks updatedSourceWatermarks =
        manager.getWatermarks(graph.getProducer(createdInts));

    assertThat(
        updatedSourceWatermarks.getOutputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));

    TransformWatermarks finishedFilterWatermarks =
        manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        finishedFilterWatermarks.getInputWatermark(),
        not(lessThan(BoundedWindow.TIMESTAMP_MAX_VALUE)));
    assertThat(
        finishedFilterWatermarks.getOutputWatermark(), not(greaterThan(new Instant(10_000L))));
  }

  @Test
  public void getSynchronizedProcessingTimeInputWatermarksHeldToPendingBundles() {
    TransformWatermarks watermarks = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(watermarks.getSynchronizedProcessingInputTime(), equalTo(clock.now()));
    assertThat(
        watermarks.getSynchronizedProcessingOutputTime(),
        equalTo(BoundedWindow.TIMESTAMP_MIN_VALUE));

    TransformWatermarks filteredWatermarks = manager.getWatermarks(graph.getProducer(filtered));
    // Non-root processing watermarks don't progress until data has been processed
    assertThat(
        filteredWatermarks.getSynchronizedProcessingInputTime(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));
    assertThat(
        filteredWatermarks.getSynchronizedProcessingOutputTime(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));

    CommittedBundle<Integer> createOutput =
        bundleFactory.createBundle(createdInts).commit(new Instant(1250L));

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createOutput),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks createAfterUpdate = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(createAfterUpdate.getSynchronizedProcessingInputTime(), equalTo(clock.now()));
    assertThat(createAfterUpdate.getSynchronizedProcessingOutputTime(), equalTo(clock.now()));

    TransformWatermarks filterAfterProduced = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        filterAfterProduced.getSynchronizedProcessingInputTime(), not(greaterThan(clock.now())));
    assertThat(
        filterAfterProduced.getSynchronizedProcessingOutputTime(), not(greaterThan(clock.now())));

    clock.set(new Instant(1500L));
    assertThat(createAfterUpdate.getSynchronizedProcessingInputTime(), equalTo(clock.now()));
    assertThat(createAfterUpdate.getSynchronizedProcessingOutputTime(), equalTo(clock.now()));
    assertThat(
        filterAfterProduced.getSynchronizedProcessingInputTime(),
        not(greaterThan(new Instant(1250L))));
    assertThat(
        filterAfterProduced.getSynchronizedProcessingOutputTime(),
        not(greaterThan(new Instant(1250L))));

    CommittedBundle<?> filterOutputBundle =
        bundleFactory.createBundle(intsToFlatten).commit(new Instant(1250L));
    manager.updateWatermarks(
        createOutput,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        createOutput.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filterOutputBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks filterAfterConsumed = manager.getWatermarks(graph.getProducer(filtered));
    assertThat(
        filterAfterConsumed.getSynchronizedProcessingInputTime(),
        not(greaterThan(createAfterUpdate.getSynchronizedProcessingOutputTime())));
    assertThat(
        filterAfterConsumed.getSynchronizedProcessingOutputTime(),
        not(greaterThan(filterAfterConsumed.getSynchronizedProcessingInputTime())));
  }

  /**
   * Demonstrates that the Synchronized Processing Time output watermark cannot progress past
   * pending timers in the same set. This propagates to all downstream SynchronizedProcessingTimes.
   *
   * <p>Also demonstrate that the result is monotonic.
   */
  @Test
  public void getSynchronizedProcessingTimeOutputHeldToPendingTimers() {
    CommittedBundle<Integer> createdBundle = multiWindowedBundle(createdInts, 1, 2, 4, 8);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createdBundle),
        new Instant(1248L));
    manager.refreshAll();

    TransformWatermarks filteredWms = manager.getWatermarks(graph.getProducer(filtered));
    TransformWatermarks filteredDoubledWms =
        manager.getWatermarks(graph.getProducer(filteredTimesTwo));
    Instant initialFilteredWm = filteredWms.getSynchronizedProcessingOutputTime();
    Instant initialFilteredDoubledWm = filteredDoubledWms.getSynchronizedProcessingOutputTime();

    StructuralKey<String> key = StructuralKey.of("key", StringUtf8Coder.of());
    CommittedBundle<Integer> filteredBundle = multiWindowedBundle(filtered, 2, 8);
    TimerData pastTimer =
        TimerData.of(StateNamespaces.global(), new Instant(250L), TimeDomain.PROCESSING_TIME);
    TimerData futureTimer =
        TimerData.of(StateNamespaces.global(), new Instant(4096L), TimeDomain.PROCESSING_TIME);
    TimerUpdate timers = TimerUpdate.builder(key).setTimer(pastTimer).setTimer(futureTimer).build();
    manager.updateWatermarks(
        createdBundle,
        timers,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filteredBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    Instant startTime = clock.now();
    clock.set(startTime.plus(250L));
    // We're held based on the past timer
    assertThat(filteredWms.getSynchronizedProcessingOutputTime(), not(greaterThan(startTime)));
    assertThat(
        filteredDoubledWms.getSynchronizedProcessingOutputTime(), not(greaterThan(startTime)));
    // And we're monotonic
    assertThat(filteredWms.getSynchronizedProcessingOutputTime(), not(lessThan(initialFilteredWm)));
    assertThat(
        filteredDoubledWms.getSynchronizedProcessingOutputTime(),
        not(lessThan(initialFilteredDoubledWm)));

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firedTimers = manager.extractFiredTimers();
    assertThat(Iterables.getOnlyElement(firedTimers).getTimers(), contains(pastTimer));
    // Our timer has fired, but has not been completed, so it holds our synchronized processing WM
    assertThat(filteredWms.getSynchronizedProcessingOutputTime(), not(greaterThan(startTime)));
    assertThat(
        filteredDoubledWms.getSynchronizedProcessingOutputTime(), not(greaterThan(startTime)));

    CommittedBundle<Integer> filteredTimerBundle =
        bundleFactory.createKeyedBundle(key, filtered).commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
    CommittedBundle<Integer> filteredTimerResult =
        bundleFactory
            .createKeyedBundle(key, filteredTimesTwo)
            .commit(filteredWms.getSynchronizedProcessingOutputTime());
    // Complete the processing time timer
    manager.updateWatermarks(
        filteredTimerBundle,
        TimerUpdate.builder(key).withCompletedTimers(Collections.singleton(pastTimer)).build(),
        graph.getProducer(filtered),
        filteredTimerBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filteredTimerResult),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    clock.set(startTime.plus(500L));
    assertThat(filteredWms.getSynchronizedProcessingOutputTime(), not(greaterThan(clock.now())));
    // filtered should be held to the time at which the filteredTimerResult fired
    assertThat(
        filteredDoubledWms.getSynchronizedProcessingOutputTime(),
        not(lessThan(filteredTimerResult.getSynchronizedProcessingOutputWatermark())));

    manager.updateWatermarks(
        filteredTimerResult,
        TimerUpdate.empty(),
        graph.getProducer(filteredTimesTwo),
        filteredTimerResult.withElements(Collections.emptyList()),
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    assertThat(filteredDoubledWms.getSynchronizedProcessingOutputTime(), equalTo(clock.now()));

    clock.set(new Instant(Long.MAX_VALUE));
    assertThat(filteredWms.getSynchronizedProcessingOutputTime(), equalTo(new Instant(4096)));
    assertThat(
        filteredDoubledWms.getSynchronizedProcessingOutputTime(),
        not(greaterThan(new Instant(4096))));
  }

  /**
   * Demonstrates that if any earlier processing holds appear in the synchronized processing time
   * output hold the result is monotonic.
   */
  @Test
  public void getSynchronizedProcessingTimeOutputTimeIsMonotonic() {
    Instant startTime = clock.now();
    TransformWatermarks watermarks = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(watermarks.getSynchronizedProcessingInputTime(), equalTo(startTime));

    TransformWatermarks filteredWatermarks = manager.getWatermarks(graph.getProducer(filtered));
    // Non-root processing watermarks don't progress until data has been processed
    assertThat(
        filteredWatermarks.getSynchronizedProcessingInputTime(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));
    assertThat(
        filteredWatermarks.getSynchronizedProcessingOutputTime(),
        not(greaterThan(BoundedWindow.TIMESTAMP_MIN_VALUE)));

    CommittedBundle<Integer> createOutput =
        bundleFactory.createBundle(createdInts).commit(new Instant(1250L));

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createOutput),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();
    TransformWatermarks createAfterUpdate = manager.getWatermarks(graph.getProducer(createdInts));
    assertThat(
        createAfterUpdate.getSynchronizedProcessingInputTime(), not(greaterThan(clock.now())));
    assertThat(
        createAfterUpdate.getSynchronizedProcessingOutputTime(), not(greaterThan(clock.now())));

    CommittedBundle<Integer> createSecondOutput =
        bundleFactory.createBundle(createdInts).commit(new Instant(750L));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(createSecondOutput),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    assertThat(createAfterUpdate.getSynchronizedProcessingOutputTime(), equalTo(clock.now()));
  }

  @Test
  public void synchronizedProcessingInputTimeIsHeldToUpstreamProcessingTimeTimers() {
    CommittedBundle<Integer> created = multiWindowedBundle(createdInts, 1, 2, 3);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(created),
        new Instant(40_900L));
    manager.refreshAll();

    CommittedBundle<Integer> filteredBundle = multiWindowedBundle(filtered, 2, 4);
    Instant upstreamHold = new Instant(2048L);
    TimerData upstreamProcessingTimer =
        TimerData.of(StateNamespaces.global(), upstreamHold, TimeDomain.PROCESSING_TIME);
    manager.updateWatermarks(
        created,
        TimerUpdate.builder(StructuralKey.of("key", StringUtf8Coder.of()))
            .setTimer(upstreamProcessingTimer)
            .build(),
        graph.getProducer(filtered),
        created.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filteredBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    TransformWatermarks downstreamWms = manager.getWatermarks(graph.getProducer(filteredTimesTwo));
    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), equalTo(clock.now()));

    clock.set(BoundedWindow.TIMESTAMP_MAX_VALUE);
    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), equalTo(upstreamHold));

    manager.extractFiredTimers();
    // Pending processing time timers that have been fired but aren't completed hold the
    // synchronized processing time
    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), equalTo(upstreamHold));

    CommittedBundle<Integer> otherCreated = multiWindowedBundle(createdInts, 4, 8, 12);
    manager.updateWatermarks(
        otherCreated,
        TimerUpdate.builder(StructuralKey.of("key", StringUtf8Coder.of()))
            .withCompletedTimers(Collections.singleton(upstreamProcessingTimer))
            .build(),
        graph.getProducer(filtered),
        otherCreated.withElements(Collections.emptyList()),
        Collections.emptyList(),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), not(lessThan(clock.now())));
  }

  @Test
  public void synchronizedProcessingInputTimeIsHeldToPendingBundleTimes() {
    CommittedBundle<Integer> created = multiWindowedBundle(createdInts, 1, 2, 3);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.<CommittedBundle<?>>singleton(created),
        new Instant(29_919_235L));

    Instant upstreamHold = new Instant(2048L);
    CommittedBundle<Integer> filteredBundle =
        bundleFactory
            .createKeyedBundle(StructuralKey.of("key", StringUtf8Coder.of()), filtered)
            .commit(upstreamHold);
    manager.updateWatermarks(
        created,
        TimerUpdate.empty(),
        graph.getProducer(filtered),
        created.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(filteredBundle),
        BoundedWindow.TIMESTAMP_MAX_VALUE);
    manager.refreshAll();

    TransformWatermarks downstreamWms = manager.getWatermarks(graph.getProducer(filteredTimesTwo));
    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), equalTo(clock.now()));

    clock.set(BoundedWindow.TIMESTAMP_MAX_VALUE);
    assertThat(downstreamWms.getSynchronizedProcessingInputTime(), equalTo(upstreamHold));
  }

  @Test
  public void extractFiredTimersReturnsFiredEventTimeTimers() {
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> initialTimers =
        manager.extractFiredTimers();
    // Watermarks haven't advanced
    assertThat(initialTimers, emptyIterable());

    // Advance WM of keyed past the first timer, but ahead of the second and third
    CommittedBundle<Integer> createdBundle = multiWindowedBundle(filtered);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.singleton(createdBundle),
        new Instant(1500L));
    manager.refreshAll();

    TimerData earliestTimer =
        TimerData.of(StateNamespaces.global(), new Instant(1000), TimeDomain.EVENT_TIME);
    TimerData middleTimer =
        TimerData.of(StateNamespaces.global(), new Instant(5000L), TimeDomain.EVENT_TIME);
    TimerData lastTimer =
        TimerData.of(StateNamespaces.global(), new Instant(10000L), TimeDomain.EVENT_TIME);
    StructuralKey<byte[]> key = StructuralKey.of(new byte[] {1, 4, 9}, ByteArrayCoder.of());
    TimerUpdate update =
        TimerUpdate.builder(key)
            .setTimer(earliestTimer)
            .setTimer(middleTimer)
            .setTimer(lastTimer)
            .build();

    manager.updateWatermarks(
        createdBundle,
        update,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(multiWindowedBundle(intsToFlatten)),
        new Instant(1000L));
    manager.refreshAll();

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firstFiredTimers =
        manager.extractFiredTimers();
    assertThat(firstFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> firstFired = Iterables.getOnlyElement(firstFiredTimers);
    assertThat(firstFired.getTimers(), contains(earliestTimer));

    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.emptyList(),
        new Instant(50_000L));
    manager.refreshAll();
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> secondFiredTimers =
        manager.extractFiredTimers();
    assertThat(secondFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> secondFired =
        Iterables.getOnlyElement(secondFiredTimers);
    // Contains, in order, middleTimer and then lastTimer
    assertThat(secondFired.getTimers(), contains(middleTimer, lastTimer));
  }

  @Test
  public void extractFiredTimersReturnsFiredProcessingTimeTimers() {
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> initialTimers =
        manager.extractFiredTimers();
    // Watermarks haven't advanced
    assertThat(initialTimers, emptyIterable());

    // Advance WM of keyed past the first timer, but ahead of the second and third
    CommittedBundle<Integer> createdBundle = multiWindowedBundle(filtered);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.singleton(createdBundle),
        new Instant(1500L));

    TimerData earliestTimer =
        TimerData.of(StateNamespaces.global(), new Instant(999L), TimeDomain.PROCESSING_TIME);
    TimerData middleTimer =
        TimerData.of(StateNamespaces.global(), new Instant(5000L), TimeDomain.PROCESSING_TIME);
    TimerData lastTimer =
        TimerData.of(StateNamespaces.global(), new Instant(10000L), TimeDomain.PROCESSING_TIME);
    StructuralKey<?> key = StructuralKey.of(-12L, VarLongCoder.of());
    TimerUpdate update =
        TimerUpdate.builder(key)
            .setTimer(lastTimer)
            .setTimer(earliestTimer)
            .setTimer(middleTimer)
            .build();

    manager.updateWatermarks(
        createdBundle,
        update,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(multiWindowedBundle(intsToFlatten)),
        new Instant(1000L));
    manager.refreshAll();

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firstFiredTimers =
        manager.extractFiredTimers();
    assertThat(firstFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> firstFired = Iterables.getOnlyElement(firstFiredTimers);
    assertThat(firstFired.getTimers(), contains(earliestTimer));

    clock.set(new Instant(50_000L));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.emptyList(),
        new Instant(50_000L));
    manager.refreshAll();
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> secondFiredTimers =
        manager.extractFiredTimers();
    assertThat(secondFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> secondFired =
        Iterables.getOnlyElement(secondFiredTimers);
    // Contains, in order, middleTimer and then lastTimer
    assertThat(secondFired.getTimers(), contains(middleTimer, lastTimer));
  }

  @Test
  public void extractFiredTimersReturnsFiredSynchronizedProcessingTimeTimers() {
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> initialTimers =
        manager.extractFiredTimers();
    // Watermarks haven't advanced
    assertThat(initialTimers, emptyIterable());

    // Advance WM of keyed past the first timer, but ahead of the second and third
    CommittedBundle<Integer> createdBundle = multiWindowedBundle(filtered);
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.singleton(createdBundle),
        new Instant(1500L));

    TimerData earliestTimer =
        TimerData.of(
            StateNamespaces.global(), new Instant(999L), TimeDomain.SYNCHRONIZED_PROCESSING_TIME);
    TimerData middleTimer =
        TimerData.of(
            StateNamespaces.global(), new Instant(5000L), TimeDomain.SYNCHRONIZED_PROCESSING_TIME);
    TimerData lastTimer =
        TimerData.of(
            StateNamespaces.global(), new Instant(10000L), TimeDomain.SYNCHRONIZED_PROCESSING_TIME);
    StructuralKey<byte[]> key = StructuralKey.of(new byte[] {2, -2, 22}, ByteArrayCoder.of());
    TimerUpdate update =
        TimerUpdate.builder(key)
            .setTimer(lastTimer)
            .setTimer(earliestTimer)
            .setTimer(middleTimer)
            .build();

    manager.updateWatermarks(
        createdBundle,
        update,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(multiWindowedBundle(intsToFlatten)),
        new Instant(1000L));
    manager.refreshAll();

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firstFiredTimers =
        manager.extractFiredTimers();
    assertThat(firstFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> firstFired = Iterables.getOnlyElement(firstFiredTimers);
    assertThat(firstFired.getTimers(), contains(earliestTimer));

    clock.set(new Instant(50_000L));
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.emptyList(),
        new Instant(50_000L));
    manager.refreshAll();
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> secondFiredTimers =
        manager.extractFiredTimers();
    assertThat(secondFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> secondFired =
        Iterables.getOnlyElement(secondFiredTimers);
    // Contains, in order, middleTimer and then lastTimer
    assertThat(secondFired.getTimers(), contains(middleTimer, lastTimer));
  }

  @Test
  public void processingTimeTimersCanBeReset() {
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> initialTimers =
        manager.extractFiredTimers();
    assertThat(initialTimers, emptyIterable());

    String timerId = "myTimer";
    StructuralKey<?> key = StructuralKey.of(-12L, VarLongCoder.of());

    TimerData initialTimer =
        TimerData.of(
            timerId, StateNamespaces.global(), new Instant(5000L), TimeDomain.PROCESSING_TIME);

    TimerData overridingTimer =
        TimerData.of(
            timerId, StateNamespaces.global(), new Instant(10000L), TimeDomain.PROCESSING_TIME);

    TimerUpdate initialUpdate = TimerUpdate.builder(key).setTimer(initialTimer).build();
    TimerUpdate overridingUpdate = TimerUpdate.builder(key).setTimer(overridingTimer).build();

    manager.updateWatermarks(
        null,
        initialUpdate,
        graph.getProducer(createdInts),
        null,
        Collections.emptyList(),
        new Instant(5000L));
    manager.refreshAll();

    // This update should override the previous timer.
    manager.updateWatermarks(
        null,
        overridingUpdate,
        graph.getProducer(createdInts),
        null,
        Collections.emptyList(),
        new Instant(10000L));

    // Set clock past the timers.
    clock.set(new Instant(50000L));
    manager.refreshAll();

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firedTimers = manager.extractFiredTimers();
    assertThat(firedTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> timers = Iterables.getOnlyElement(firedTimers);
    assertThat(timers.getTimers(), contains(overridingTimer));
  }

  @Test
  public void eventTimeTimersCanBeReset() {
    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> initialTimers =
        manager.extractFiredTimers();
    assertThat(initialTimers, emptyIterable());

    String timerId = "myTimer";
    StructuralKey<?> key = StructuralKey.of(-12L, VarLongCoder.of());

    TimerData initialTimer =
        TimerData.of(timerId, StateNamespaces.global(), new Instant(1000L), TimeDomain.EVENT_TIME);
    TimerData overridingTimer =
        TimerData.of(timerId, StateNamespaces.global(), new Instant(2000L), TimeDomain.EVENT_TIME);

    TimerUpdate initialUpdate = TimerUpdate.builder(key).setTimer(initialTimer).build();
    TimerUpdate overridingUpdate = TimerUpdate.builder(key).setTimer(overridingTimer).build();

    CommittedBundle<Integer> createdBundle = multiWindowedBundle(filtered);
    manager.updateWatermarks(
        createdBundle,
        initialUpdate,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(multiWindowedBundle(intsToFlatten)),
        new Instant(1000L));
    manager.refreshAll();

    // This update should override the previous timer.
    manager.updateWatermarks(
        createdBundle,
        overridingUpdate,
        graph.getProducer(filtered),
        createdBundle.withElements(Collections.emptyList()),
        Collections.<CommittedBundle<?>>singleton(multiWindowedBundle(intsToFlatten)),
        new Instant(1000L));
    manager.refreshAll();

    // Set WM past the timers.
    manager.updateWatermarks(
        null,
        TimerUpdate.empty(),
        graph.getProducer(createdInts),
        null,
        Collections.singleton(createdBundle),
        new Instant(3000L));
    manager.refreshAll();

    Collection<FiredTimers<AppliedPTransform<?, ?, ?>>> firstFiredTimers =
        manager.extractFiredTimers();
    assertThat(firstFiredTimers, not(emptyIterable()));
    FiredTimers<AppliedPTransform<?, ?, ?>> firstFired = Iterables.getOnlyElement(firstFiredTimers);
    assertThat(firstFired.getTimers(), contains(overridingTimer));
  }

  @Test
  public void inputWatermarkDuplicates() {
    Watermark mockWatermark = Mockito.mock(Watermark.class);

    AppliedPTransformInputWatermark underTest =
        new AppliedPTransformInputWatermark("underTest", ImmutableList.of(mockWatermark));

    // Refresh
    when(mockWatermark.get()).thenReturn(new Instant(0));
    underTest.refresh();
    assertEquals(new Instant(0), underTest.get());

    // Apply a timer update
    StructuralKey<String> key = StructuralKey.of("key", StringUtf8Coder.of());
    TimerData timer1 =
        TimerData.of("a", StateNamespaces.global(), new Instant(100), TimeDomain.EVENT_TIME);
    TimerData timer2 =
        TimerData.of("a", StateNamespaces.global(), new Instant(200), TimeDomain.EVENT_TIME);
    underTest.updateTimers(TimerUpdate.builder(key).setTimer(timer1).setTimer(timer2).build());

    // Only the last timer update should be observable
    assertEquals(timer2.getTimestamp(), underTest.getEarliestTimerTimestamp());

    // Advance the input watermark
    when(mockWatermark.get()).thenReturn(new Instant(1000));
    underTest.refresh();
    assertEquals(new Instant(1000), underTest.get()); // input watermark is not held by timers

    // Examine the fired event time timers
    Map<StructuralKey<?>, List<TimerData>> fired = underTest.extractFiredEventTimeTimers();
    List<TimerData> timers = fired.get(key);
    assertNotNull(timers);
    assertThat(timers, contains(timer2));

    // Update based on timer firings
    underTest.updateTimers(TimerUpdate.builder(key).withCompletedTimers(timers).build());

    // Now we should be able to advance
    assertEquals(BoundedWindow.TIMESTAMP_MAX_VALUE, underTest.getEarliestTimerTimestamp());

    // Nothing left to fire
    fired = underTest.extractFiredEventTimeTimers();
    assertThat(fired.entrySet(), empty());
  }

  @Test
  public void timerUpdateBuilderBuildAddsAllAddedTimers() {
    TimerData set = TimerData.of(StateNamespaces.global(), new Instant(10L), TimeDomain.EVENT_TIME);
    TimerData deleted =
        TimerData.of(StateNamespaces.global(), new Instant(24L), TimeDomain.PROCESSING_TIME);
    TimerData completedOne =
        TimerData.of(
            StateNamespaces.global(), new Instant(1024L), TimeDomain.SYNCHRONIZED_PROCESSING_TIME);
    TimerData completedTwo =
        TimerData.of(StateNamespaces.global(), new Instant(2048L), TimeDomain.EVENT_TIME);

    TimerUpdate update =
        TimerUpdate.builder(StructuralKey.of("foo", StringUtf8Coder.of()))
            .withCompletedTimers(ImmutableList.of(completedOne, completedTwo))
            .setTimer(set)
            .deletedTimer(deleted)
            .build();

    assertThat(update.getCompletedTimers(), containsInAnyOrder(completedOne, completedTwo));
    assertThat(update.getSetTimers(), contains(set));
    assertThat(update.getDeletedTimers(), contains(deleted));
  }

  @Test
  public void timerUpdateBuilderWithSetAtEndOfTime() {
    Instant timerStamp = BoundedWindow.TIMESTAMP_MAX_VALUE;
    TimerData tooFar = TimerData.of(StateNamespaces.global(), timerStamp, TimeDomain.EVENT_TIME);

    TimerUpdateBuilder builder = TimerUpdate.builder(StructuralKey.empty());
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage(timerStamp.toString());
    builder.setTimer(tooFar);
  }

  @Test
  public void timerUpdateBuilderWithSetPastEndOfTime() {
    Instant timerStamp = BoundedWindow.TIMESTAMP_MAX_VALUE.plus(Duration.standardMinutes(2));
    TimerData tooFar = TimerData.of(StateNamespaces.global(), timerStamp, TimeDomain.EVENT_TIME);

    TimerUpdateBuilder builder = TimerUpdate.builder(StructuralKey.empty());
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage(timerStamp.toString());
    builder.setTimer(tooFar);
  }

  @Test
  public void timerUpdateBuilderWithSetThenDeleteHasOnlyDeleted() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.setTimer(timer).deletedTimer(timer).build();

    assertThat(built.getSetTimers(), emptyIterable());
    assertThat(built.getDeletedTimers(), contains(timer));
  }

  @Test
  public void timerUpdateBuilderWithDeleteThenSetHasOnlySet() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.deletedTimer(timer).setTimer(timer).build();

    assertThat(built.getSetTimers(), contains(timer));
    assertThat(built.getDeletedTimers(), emptyIterable());
  }

  @Test
  public void timerUpdateBuilderWithSetAfterBuildNotAddedToBuilt() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.build();
    builder.setTimer(timer);
    assertThat(built.getSetTimers(), emptyIterable());
    builder.build();
    assertThat(built.getSetTimers(), emptyIterable());
  }

  @Test
  public void timerUpdateBuilderWithDeleteAfterBuildNotAddedToBuilt() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.build();
    builder.deletedTimer(timer);
    assertThat(built.getDeletedTimers(), emptyIterable());
    builder.build();
    assertThat(built.getDeletedTimers(), emptyIterable());
  }

  @Test
  public void timerUpdateBuilderWithCompletedAfterBuildNotAddedToBuilt() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.build();
    builder.withCompletedTimers(ImmutableList.of(timer));
    assertThat(built.getCompletedTimers(), emptyIterable());
    builder.build();
    assertThat(built.getCompletedTimers(), emptyIterable());
  }

  @Test
  public void timerUpdateWithCompletedTimersNotAddedToExisting() {
    TimerUpdateBuilder builder = TimerUpdate.builder(null);
    TimerData timer = TimerData.of(StateNamespaces.global(), Instant.now(), TimeDomain.EVENT_TIME);

    TimerUpdate built = builder.build();
    assertThat(built.getCompletedTimers(), emptyIterable());
    assertThat(
        built.withCompletedTimers(ImmutableList.of(timer)).getCompletedTimers(), contains(timer));
    assertThat(built.getCompletedTimers(), emptyIterable());
  }

  @SafeVarargs
  private final <T> CommittedBundle<T> timestampedBundle(
      PCollection<T> pc, TimestampedValue<T>... values) {
    UncommittedBundle<T> bundle = bundleFactory.createBundle(pc);
    for (TimestampedValue<T> value : values) {
      bundle.add(
          WindowedValue.timestampedValueInGlobalWindow(value.getValue(), value.getTimestamp()));
    }
    return bundle.commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
  }

  @SafeVarargs
  private final <T> CommittedBundle<T> multiWindowedBundle(PCollection<T> pc, T... values) {
    UncommittedBundle<T> bundle = bundleFactory.createBundle(pc);
    Collection<BoundedWindow> windows =
        ImmutableList.of(
            GlobalWindow.INSTANCE,
            new IntervalWindow(BoundedWindow.TIMESTAMP_MIN_VALUE, new Instant(0)));
    for (T value : values) {
      bundle.add(
          WindowedValue.of(value, BoundedWindow.TIMESTAMP_MIN_VALUE, windows, PaneInfo.NO_FIRING));
    }
    return bundle.commit(BoundedWindow.TIMESTAMP_MAX_VALUE);
  }
}
