// Copyright 2022 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.starlarkbuildapi.core;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.stream;

import com.google.devtools.build.lib.cmdline.BazelCompileContext;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.LabelSyntaxException;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import net.starlark.java.eval.GuardedValue;
import net.starlark.java.eval.StarlarkSemantics;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

@RunWith(JUnit4.class)
public final class ContextGuardedValueTest {

  /**
   * We want to make sure the empty string doesn't result in "allow everything". That would be bad.
   * Most allowlists have an entry like ("", "tools/build_defs/lang") for usage within Google.
   */
  @Test
  public void emptyRepoAllowed_doesntMatchNonAllowed() throws Exception {
    assertNotAllowed("@mylang//bar:baz", "@//tools/lang");
  }

  @Test
  public void emptyRepoAllowed_matchesAllowed() throws Exception {
    assertAllowed("@//tools/lang", "@//tools/lang");
  }

  @Test
  public void workspaceRepo_matchesAllowedRepo() throws Exception {
    assertAllowed("@rules_foo//tools/lang", "@rules_foo//");
  }

  @Test
  public void workspaceRepo_doesntMatchCommonSubstr() throws Exception {
    assertNotAllowed("@my_rules_foo_helper//tools/lang", "@rules_foo//");
  }

  @Test
  public void bzlmodRepo_matchesStart() throws Exception {
    assertAllowed("@rules_foo~override//tools/lang", "@rules_foo//");
    assertAllowed("@rules_foo~1.2.3//tools/lang", "@rules_foo//");
  }

  @Test
  public void bzlmodRepo_matchesWithin() throws Exception {
    assertAllowed("@rules_lang~override~ext~foo_helper//tools/lang", "@foo_helper//");
  }

  @Test
  public void bzlmodRepo_doesntMatchCommonSubstr() throws Exception {
    assertNotAllowed("@rules_lang~override~ext~my_foo_helper_lib//tools/lang", "@foo_helper//");
  }

  @Test
  public void reposWithDotsDontMatch() throws Exception {
    assertNotAllowed("@my.lang//foo", "@my_lang//");
  }

  @Test
  public void verifySomeRealisticCases() throws Exception {
    // Python with workspace
    assertAllowed("@//tools/build_defs/python/private", "@//tools/build_defs/python");
    assertAllowed("@rules_python//python/private", "@rules_python//");

    // Python with bzlmod
    assertAllowed(
        "@rules_python~override~internal_deps~rules_python_internal//private", "@rules_python//");

    // CC with workspace
    assertAllowed("@//tools/build_defs/cc", "@//tools/build_defs/cc");
    assertNotAllowed("@rules_cc_helper//tools/build_defs/cc", "@rules_cc//");

    // CC with Bzlmod
    assertAllowed("@rules_cc~1.2.3~ext_name~local_cc_config//foo", "@local_cc_config//");
  }

  private Object createClientData(String callerLabelStr) {
    return BazelCompileContext.create(
        Label.parseCanonicalUnchecked(callerLabelStr), "unused_caller.bzl");
  }

  private GuardedValue createGuard(Object clientData, String... allowedLabelStrs) throws Exception {
    var allowed =
        stream(allowedLabelStrs)
            .map(
                labelStr -> {
                  try {
                    return PackageIdentifier.parse(labelStr);
                  } catch (LabelSyntaxException e) {
                    // We have to manually catch and re-throw this, otherwise Java is unhappy.
                    throw new RuntimeException(e);
                  }
                })
            .collect(toImmutableSet());

    return ContextGuardedValue.onlyInAllowedRepos(clientData, allowed);
  }

  private void assertAllowed(String callerLabelStr, String... allowedLabelStrs) throws Exception {
    var clientData = createClientData(callerLabelStr);
    var guard = createGuard(clientData, allowedLabelStrs);
    assertThat(guard.isObjectAccessibleUsingSemantics(StarlarkSemantics.DEFAULT, clientData))
        .isTrue();
  }

  private void assertNotAllowed(String callerLabelStr, String... allowedLabelStrs)
      throws Exception {
    var clientData = createClientData(callerLabelStr);
    var guard = createGuard(clientData, allowedLabelStrs);
    assertThat(guard.isObjectAccessibleUsingSemantics(StarlarkSemantics.DEFAULT, clientData))
        .isFalse();
  }
}
