// Copyright 2015 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.build.lib.skyframe;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.devtools.build.lib.events.util.EventCollectionApparatus;
import com.google.devtools.build.lib.skyframe.DiffAwarenessManager.ProcessableModifiedFileSet;
import com.google.devtools.build.lib.vfs.FileSystem;
import com.google.devtools.build.lib.vfs.ModifiedFileSet;
import com.google.devtools.build.lib.vfs.PathFragment;
import com.google.devtools.build.lib.vfs.Root;
import com.google.devtools.build.lib.vfs.inmemoryfs.InMemoryFileSystem;
import com.google.devtools.common.options.OptionsClassProvider;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/**
 * Unit tests for {@link DiffAwarenessManager}, especially of the fact that it works in a sequential
 * manner and of its correctness in the presence of unprocesed diffs.
 */
@RunWith(JUnit4.class)
public class DiffAwarenessManagerTest {
  private FileSystem fs;
  protected EventCollectionApparatus events;

  @Before
  public final void createFileSystem() throws Exception  {
    fs = new InMemoryFileSystem();
  }

  @Before
  public final void initializeEventCollectionApparatus() {
    events = new EventCollectionApparatus();
    events.setFailFast(false);
  }

  @Test
  public void testEverythingModifiedIfNoDiffAwareness() throws Exception {
    Root pathEntry = Root.fromPath(fs.getPath("/pathEntry"));
    DiffAwarenessFactoryStub factory = new DiffAwarenessFactoryStub();
    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
    assertWithMessage("Expected EVERYTHING_MODIFIED since there are no factories")
        .that(
            manager
                .getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY)
                .getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    events.assertNoWarningsOrErrors();
  }

  @Test
  public void testResetAndSetPathEntriesCallClose() throws Exception {
    Root pathEntry = Root.fromPath(fs.getPath("/pathEntry"));
    ModifiedFileSet diff = ModifiedFileSet.NOTHING_MODIFIED;
    DiffAwarenessStub diffAwareness1 = new DiffAwarenessStub(ImmutableList.of(diff));
    DiffAwarenessStub diffAwareness2 = new DiffAwarenessStub(ImmutableList.of(diff));
    DiffAwarenessFactoryStub factory = new DiffAwarenessFactoryStub();
    factory.inject(pathEntry, diffAwareness1);
    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
    manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertWithMessage("diffAwareness1 shouldn't have been closed yet")
        .that(diffAwareness1.closed())
        .isFalse();
    manager.reset();
    assertWithMessage("diffAwareness1 should have been closed by reset")
        .that(diffAwareness1.closed())
        .isTrue();
    factory.inject(pathEntry, diffAwareness2);
    manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertWithMessage("diffAwareness2 shouldn't have been closed yet")
        .that(diffAwareness2.closed())
        .isFalse();
    events.assertNoWarningsOrErrors();
  }

  @Test
  public void testHandlesUnprocessedDiffs() throws Exception {
    Root pathEntry = Root.fromPath(fs.getPath("/pathEntry"));
    ModifiedFileSet diff1 = ModifiedFileSet.builder().modify(PathFragment.create("file1")).build();
    ModifiedFileSet diff2 = ModifiedFileSet.builder().modify(PathFragment.create("file2")).build();
    ModifiedFileSet diff3 = ModifiedFileSet.builder().modify(PathFragment.create("file3")).build();
    DiffAwarenessStub diffAwareness =
        new DiffAwarenessStub(ImmutableList.of(diff1, diff2, diff3, DiffAwarenessStub.BROKEN_DIFF));
    DiffAwarenessFactoryStub factory = new DiffAwarenessFactoryStub();
    factory.inject(pathEntry, diffAwareness);
    DiffAwarenessManager manager = new DiffAwarenessManager(ImmutableList.of(factory));
    ProcessableModifiedFileSet firstProcessableDiff =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertWithMessage("Expected EVERYTHING_MODIFIED on first call to getDiff")
        .that(firstProcessableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    firstProcessableDiff.markProcessed();
    ProcessableModifiedFileSet processableDiff1 =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff1.getModifiedFileSet()).isEqualTo(diff1);
    ProcessableModifiedFileSet processableDiff2 =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff2.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.union(diff1, diff2));
    processableDiff2.markProcessed();
    ProcessableModifiedFileSet processableDiff3 =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff3.getModifiedFileSet()).isEqualTo(diff3);
    events.assertNoWarningsOrErrors();
    ProcessableModifiedFileSet processableDiff4 =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff4.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    events.assertContainsWarning("error");
  }

  @Test
  public void testHandlesBrokenDiffs() throws Exception {
    Root pathEntry = Root.fromPath(fs.getPath("/pathEntry"));
    DiffAwarenessFactoryStub factory1 = new DiffAwarenessFactoryStub();
    DiffAwarenessStub diffAwareness1 =
        new DiffAwarenessStub(ImmutableList.<ModifiedFileSet>of(), 1);
    factory1.inject(pathEntry, diffAwareness1);
    DiffAwarenessFactoryStub factory2 = new DiffAwarenessFactoryStub();
    ModifiedFileSet diff2 = ModifiedFileSet.builder().modify(PathFragment.create("file2")).build();
    DiffAwarenessStub diffAwareness2 =
        new DiffAwarenessStub(ImmutableList.of(diff2, DiffAwarenessStub.BROKEN_DIFF));
    factory2.inject(pathEntry, diffAwareness2);
    DiffAwarenessFactoryStub factory3 = new DiffAwarenessFactoryStub();
    ModifiedFileSet diff3 = ModifiedFileSet.builder().modify(PathFragment.create("file3")).build();
    DiffAwarenessStub diffAwareness3 = new DiffAwarenessStub(ImmutableList.of(diff3));
    factory3.inject(pathEntry, diffAwareness3);
    DiffAwarenessManager manager =
        new DiffAwarenessManager(ImmutableList.of(factory1, factory2, factory3));

    ProcessableModifiedFileSet processableDiff =
        manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    events.assertNoWarningsOrErrors();
    assertWithMessage("Expected EVERYTHING_MODIFIED on first call to getDiff for diffAwareness1")
        .that(processableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    processableDiff.markProcessed();

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    events.assertContainsEventWithFrequency("error in getCurrentView", 1);
    assertWithMessage("Expected EVERYTHING_MODIFIED because of broken getCurrentView")
        .that(processableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    processableDiff.markProcessed();
    factory1.remove(pathEntry);

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertWithMessage("Expected EVERYTHING_MODIFIED on first call to getDiff for diffAwareness2")
        .that(processableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    processableDiff.markProcessed();

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff.getModifiedFileSet()).isEqualTo(diff2);
    processableDiff.markProcessed();

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    events.assertContainsEventWithFrequency("error in getDiff", 1);
    assertWithMessage("Expected EVERYTHING_MODIFIED because of broken getDiff")
        .that(processableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    processableDiff.markProcessed();
    factory2.remove(pathEntry);

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertWithMessage("Expected EVERYTHING_MODIFIED on first call to getDiff for diffAwareness3")
        .that(processableDiff.getModifiedFileSet())
        .isEqualTo(ModifiedFileSet.EVERYTHING_MODIFIED);
    processableDiff.markProcessed();

    processableDiff = manager.getDiff(events.reporter(), pathEntry, OptionsClassProvider.EMPTY);
    assertThat(processableDiff.getModifiedFileSet()).isEqualTo(diff3);
    processableDiff.markProcessed();
  }

  private static class DiffAwarenessFactoryStub implements DiffAwareness.Factory {

    private final Map<Root, DiffAwareness> diffAwarenesses = Maps.newHashMap();

    public void inject(Root pathEntry, DiffAwareness diffAwareness) {
      diffAwarenesses.put(pathEntry, diffAwareness);
    }

    public void remove(Root pathEntry) {
      diffAwarenesses.remove(pathEntry);
    }

    @Override
    @Nullable
    public DiffAwareness maybeCreate(Root pathEntry) {
      return diffAwarenesses.get(pathEntry);
    }
  }

  private static class DiffAwarenessStub implements DiffAwareness {

    public static final ModifiedFileSet BROKEN_DIFF =
        ModifiedFileSet.builder().modify(PathFragment.create("special broken marker")).build();

    private boolean closed = false;
    private int curSequenceNum = 0;
    private final List<ModifiedFileSet> sequentialDiffs;
    private final int brokenViewNum;

    public DiffAwarenessStub(List<ModifiedFileSet> sequentialDiffs) {
      this(sequentialDiffs, -1);
    }

    public DiffAwarenessStub(List<ModifiedFileSet> sequentialDiffs, int brokenViewNum) {
      this.sequentialDiffs = sequentialDiffs;
      this.brokenViewNum = brokenViewNum;
    }

    private static class ViewStub implements DiffAwareness.View {
      private final int sequenceNum;

      public ViewStub(int sequenceNum) {
        this.sequenceNum = sequenceNum;
      }
    }

    @Override
    public View getCurrentView(OptionsClassProvider options) throws BrokenDiffAwarenessException {
      if (curSequenceNum == brokenViewNum) {
        throw new BrokenDiffAwarenessException("error in getCurrentView");
      }
      return new ViewStub(curSequenceNum++);
    }

    @Override
    public ModifiedFileSet getDiff(View oldView, View newView) throws BrokenDiffAwarenessException {
      assertThat(oldView).isInstanceOf(ViewStub.class);
      assertThat(newView).isInstanceOf(ViewStub.class);
      ViewStub oldViewStub = (ViewStub) oldView;
      ViewStub newViewStub = (ViewStub) newView;
      Preconditions.checkState(newViewStub.sequenceNum >= oldViewStub.sequenceNum);
      ModifiedFileSet diff = ModifiedFileSet.NOTHING_MODIFIED;
      for (int num = oldViewStub.sequenceNum; num < newViewStub.sequenceNum; num++) {
        ModifiedFileSet incrementalDiff = sequentialDiffs.get(num);
        if (incrementalDiff == BROKEN_DIFF) {
          throw new BrokenDiffAwarenessException("error in getDiff");
        }
        diff = ModifiedFileSet.union(diff, incrementalDiff);
      }
      return diff;
    }

    @Override
    public String name() {
      return "testingstub";
    }

    @Override
    public void close() {
      closed = true;
    }

    public boolean closed() {
      return closed;
    }
  }
}
