// Copyright 2017 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.collect.nestedset;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Objects;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.devtools.build.lib.actions.CommandLineItem;
import com.google.devtools.build.lib.actions.CommandLineItem.CapturingMapFn;
import com.google.devtools.build.lib.actions.CommandLineItem.MapFn;
import com.google.devtools.build.lib.util.Fingerprint;
import java.util.function.Consumer;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link NestedSetFingerprintCache}. */
@RunWith(JUnit4.class)
public class NestedSetFingerprintCacheTest {

  private class TestNestedSetFingerprintCache extends NestedSetFingerprintCache {
    private Multiset<Object> fingerprinted = HashMultiset.create();

    @Override
    <T> void addToFingerprint(MapFn<? super T> mapFn, Fingerprint fingerprint, T object) {
      super.addToFingerprint(mapFn, fingerprint, object);
      fingerprinted.add(object);
    }
  }

  private TestNestedSetFingerprintCache cache;

  @Before
  public void setup() {
    cache = new TestNestedSetFingerprintCache();
  }

  @Test
  public void testBasic() {
    NestedSet<String> nestedSet = NestedSetBuilder.<String>stableOrder().add("a").add("b").build();

    // This test does reimplement the inner algorithm of the cache, but serves
    // as a simple check that the basic operations do something sensible
    Fingerprint fingerprint = new Fingerprint();
    fingerprint.addInt(nestedSet.getOrder().ordinal());
    Fingerprint subFingerprint = new Fingerprint();
    subFingerprint.addString("a");
    subFingerprint.addString("b");
    fingerprint.addBytes(subFingerprint.digestAndReset());
    String controlDigest = fingerprint.hexDigestAndReset();

    Fingerprint nestedSetFingerprint = new Fingerprint();
    cache.addNestedSetToFingerprint(nestedSetFingerprint, nestedSet);
    String nestedSetDigest = nestedSetFingerprint.hexDigestAndReset();

    assertThat(controlDigest).isEqualTo(nestedSetDigest);
  }

  @Test
  public void testOnlyFingerprintedOncePerString() {
    // Leaving leaf nodes with a single item will defeat this check
    // The nested set builder will effectively inline single-item objects into their parent,
    // meaning they will get hashed multiple times.
    NestedSet<String> a = NestedSetBuilder.<String>stableOrder().add("a0").add("a1").build();
    NestedSet<String> b = NestedSetBuilder.<String>stableOrder().add("b0").add("b1").build();
    NestedSet<String> c =
        NestedSetBuilder.<String>stableOrder().add("c").addTransitive(a).addTransitive(b).build();
    NestedSet<String> d =
        NestedSetBuilder.<String>stableOrder().add("d").addTransitive(a).addTransitive(b).build();
    NestedSet<String> e =
        NestedSetBuilder.<String>stableOrder().add("e").addTransitive(c).addTransitive(d).build();
    cache.addNestedSetToFingerprint(new Fingerprint(), e);
    assertThat(cache.fingerprinted.elementSet())
        .containsExactly("a0", "a1", "b0", "b1", "c", "d", "e");
    for (Multiset.Entry<Object> entry : cache.fingerprinted.entrySet()) {
      assertThat(entry.getCount()).isEqualTo(1);
    }
  }

  @Test
  public void testMapFn() {
    // Make sure that the map function assigns completely different key spaces
    NestedSet<String> a = NestedSetBuilder.<String>stableOrder().add("a0").add("a1").build();

    Fingerprint defaultMapFnFingerprint = new Fingerprint();
    cache.addNestedSetToFingerprint(defaultMapFnFingerprint, a);
    Fingerprint explicitDefaultMapFnFingerprint = new Fingerprint();
    cache.addNestedSetToFingerprint(
        CommandLineItem.MapFn.DEFAULT, explicitDefaultMapFnFingerprint, a);
    Fingerprint mappedFingerprint = new Fingerprint();
    cache.addNestedSetToFingerprint((s, args) -> args.accept(s + "_mapped"), mappedFingerprint, a);

    String defaultMapFnDigest = defaultMapFnFingerprint.hexDigestAndReset();
    String explicitDefaultMapFnDigest = explicitDefaultMapFnFingerprint.hexDigestAndReset();
    String mappedDigest = mappedFingerprint.hexDigestAndReset();
    assertThat(defaultMapFnDigest).isEqualTo(explicitDefaultMapFnDigest);
    assertThat(mappedDigest).isNotEqualTo(defaultMapFnDigest);

    assertThat(cache.fingerprinted.elementSet()).containsExactly("a0", "a1");
    for (Multiset.Entry<Object> entry : cache.fingerprinted.entrySet()) {
      assertThat(entry.getCount()).isEqualTo(2);
    }
  }

  @Test
  public void testMultipleInstancesOfMapFnThrows() {
    NestedSet<String> nestedSet =
        NestedSetBuilder.<String>stableOrder().add("a0").add("a1").build();

    // Make sure a normal method reference doesn't get blacklisted.
    for (int i = 0; i < 2; ++i) {
      cache.addNestedSetToFingerprint(
          NestedSetFingerprintCacheTest::simpleExpand, new Fingerprint(), nestedSet);
    }

    // Try again to make sure Java synthesizes a new class for a second method reference.
    for (int i = 0; i < 2; ++i) {
      cache.addNestedSetToFingerprint(
          NestedSetFingerprintCacheTest::simpleExpand2, new Fingerprint(), nestedSet);
    }

    // Make sure a non-capturing lambda doesn't get blacklisted
    for (int i = 0; i < 2; ++i) {
      cache.addNestedSetToFingerprint(
          (s, args) -> args.accept(s + "_mapped"), new Fingerprint(), nestedSet);
    }

    // Make sure a CapturingMapFn doesn't get blacklisted
    for (int i = 0; i < 2; ++i) {
      cache.addNestedSetToFingerprint(
          (CapturingMapFn<String>) (s, args) -> args.accept(s + 1), new Fingerprint(), nestedSet);
    }

    // Make sure a ParametrizedMapFn doesn't get blacklisted until it exceeds its instance count
    cache.addNestedSetToFingerprint(new IntParametrizedMapFn(1), new Fingerprint(), nestedSet);
    cache.addNestedSetToFingerprint(new IntParametrizedMapFn(2), new Fingerprint(), nestedSet);
    assertThrows(
        IllegalArgumentException.class,
        () ->
            cache.addNestedSetToFingerprint(
                new IntParametrizedMapFn(3), new Fingerprint(), nestedSet));

    // Make sure a capturing method reference gets blacklisted. The for loop causes the variable i
    // to be captured, so that str::expand becomes a capturing lambda, not a plain method reference.
    // This test case ensures that the captured lambda cannot be used twice.
    assertThrows(
        IllegalArgumentException.class,
        () -> {
          for (int i = 0; i < 2; ++i) {
            StringJoiner str = new StringJoiner("hello");
            cache.addNestedSetToFingerprint(str::expand, new Fingerprint(), nestedSet);
          }
        });

    // Do make sure that a capturing lambda gets blacklisted. The loop exists for the same reason as
    // the above case.
    assertThrows(
        IllegalArgumentException.class,
        () -> {
          for (int i = 0; i < 2; ++i) {
            final int capturedVariable = i;
            cache.addNestedSetToFingerprint(
                (s, args) -> args.accept(s + capturedVariable), new Fingerprint(), nestedSet);
          }
        });
  }

  private static class IntParametrizedMapFn extends CommandLineItem.ParametrizedMapFn<String> {
    private final int i;

    private IntParametrizedMapFn(int i) {
      this.i = i;
    }

    @Override
    public void expandToCommandLine(String object, Consumer<String> args) {
      args.accept(object + i);
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      IntParametrizedMapFn that = (IntParametrizedMapFn) o;
      return i == that.i;
    }

    @Override
    public int maxInstancesAllowed() {
      return 2;
    }

    @Override
    public int hashCode() {
      return Objects.hashCode(i);
    }
  }

  private static class StringJoiner {
    private final String str;

    private StringJoiner(String str) {
      this.str = str;
    }

    private void expand(String other, Consumer<String> args) {
      args.accept(str + other);
    }
  }

  private static void simpleExpand(String o, Consumer<String> args) {
    args.accept(o + "_mapped");
  }

  private static void simpleExpand2(String o, Consumer<String> args) {
    args.accept(o + "_mapped2");
  }
}
