// 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.packages;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.devtools.build.lib.analysis.ConfiguredRuleClassProvider;
import com.google.devtools.build.lib.cmdline.Label;
import com.google.devtools.build.lib.cmdline.PackageIdentifier;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.events.Reporter;
import com.google.devtools.build.lib.packages.RuleFactory.BuildLangTypedAttributeValuesMap;
import com.google.devtools.build.lib.packages.util.PackageLoadingTestCase;
import com.google.devtools.build.lib.syntax.Type;
import com.google.devtools.build.lib.testutil.TestRuleClassProvider;
import com.google.devtools.build.lib.vfs.Path;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.HashMap;
import java.util.Map;

@RunWith(JUnit4.class)
public class RuleFactoryTest extends PackageLoadingTestCase {

  private ConfiguredRuleClassProvider provider = TestRuleClassProvider.getRuleClassProvider();
  private RuleFactory ruleFactory =
      new RuleFactory(provider, AttributeContainer.ATTRIBUTE_CONTAINER_FACTORY);

  public static final Location LOCATION_42 = Location.fromFileAndOffsets(null, 42, 42);

  @Test
  public void testCreateRule() throws Exception {
    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
    Package.Builder pkgBuilder =
        packageFactory.newPackageBuilder(PackageIdentifier.createInMainRepo("mypkg"), "TESTING")
            .setFilename(myPkgPath)
            .setMakeEnv(new MakeEnvironment.Builder());

    Map<String, Object> attributeValues = new HashMap<>();
    attributeValues.put("name", "foo");
    attributeValues.put("alwayslink", true);

    RuleClass ruleClass = provider.getRuleClassMap().get("cc_library");
    Rule rule =
        RuleFactory.createAndAddRule(
            pkgBuilder,
            ruleClass,
            new BuildLangTypedAttributeValuesMap(attributeValues),
            new Reporter(),
            /*ast=*/ null,
            LOCATION_42,
            /*env=*/ null,
            new AttributeContainer(ruleClass));

    assertSame(rule, rule.getAssociatedRule());

    // pkg.getRules() = [rule]
    Package pkg = pkgBuilder.build();
    assertThat(Sets.newHashSet(pkg.getTargets(Rule.class))).hasSize(1);
    assertEquals(rule, pkg.getTargets(Rule.class).iterator().next());

    assertSame(rule, pkg.getTarget("foo"));

    assertEquals(Label.parseAbsolute("//mypkg:foo"), rule.getLabel());
    assertEquals("foo", rule.getName());

    assertEquals("cc_library", rule.getRuleClass());
    assertEquals("cc_library rule", rule.getTargetKind());
    assertEquals(42, rule.getLocation().getStartOffset());
    assertFalse(rule.containsErrors());

    // Attr with explicitly-supplied value:
    AttributeMap attributes = RawAttributeMapper.of(rule);
    assertTrue(attributes.get("alwayslink", Type.BOOLEAN));
    try {
      attributes.get("alwayslink", Type.STRING); // type error: boolean, not string!
      fail();
    } catch (Exception e) {
      /* Class of exception and error message are not specified by API. */
    }
    try {
      attributes.get("nosuchattr", Type.STRING); // no such attribute
      fail();
    } catch (Exception e) {
      /* Class of exception and error message are not specified by API. */
    }

    // Attrs with default values:
    // cc_library linkstatic default=0 according to build encyc.
    assertFalse(attributes.get("linkstatic", Type.BOOLEAN));
    assertFalse(attributes.get("testonly", Type.BOOLEAN));
    assertThat(attributes.get("srcs", BuildType.LABEL_LIST)).isEmpty();
  }

  @Test
  public void testCreateWorkspaceRule() throws Exception {
    Path myPkgPath = scratch.resolve("/foo/workspace/WORKSPACE");
    Package.Builder pkgBuilder = packageFactory.newExternalPackageBuilder(myPkgPath, "TESTING");

    Map<String, Object> attributeValues = new HashMap<>();
    attributeValues.put("name", "foo");
    attributeValues.put("actual", "//foo:bar");

    RuleClass ruleClass = provider.getRuleClassMap().get("bind");
    Rule rule =
        RuleFactory.createAndAddRule(
            pkgBuilder,
            ruleClass,
            new BuildLangTypedAttributeValuesMap(attributeValues),
            new Reporter(),
            /*ast=*/ null,
            Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
            /*env=*/ null,
            new AttributeContainer(ruleClass));
    assertFalse(rule.containsErrors());
  }

  @Test
  public void testWorkspaceRuleFailsInBuildFile() throws Exception {
    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
    Package.Builder pkgBuilder =
        packageFactory.newPackageBuilder(PackageIdentifier.createInMainRepo("mypkg"), "TESTING")
            .setFilename(myPkgPath)
            .setMakeEnv(new MakeEnvironment.Builder());

    Map<String, Object> attributeValues = new HashMap<>();
    attributeValues.put("name", "foo");
    attributeValues.put("actual", "//bar:baz");

    RuleClass ruleClass = provider.getRuleClassMap().get("bind");
    try {
      RuleFactory.createAndAddRule(
          pkgBuilder,
          ruleClass,
          new BuildLangTypedAttributeValuesMap(attributeValues),
          new Reporter(),
          /*ast=*/ null,
          LOCATION_42,
          /*env=*/ null,
          new AttributeContainer(ruleClass));
      fail();
    } catch (RuleFactory.InvalidRuleException e) {
      assertThat(e.getMessage()).contains("must be in the WORKSPACE file");
    }
  }

  @Test
  public void testBuildRuleFailsInWorkspaceFile() throws Exception {
    Path myPkgPath = scratch.resolve("/foo/workspace/WORKSPACE");
    Package.Builder pkgBuilder =
        packageFactory.newPackageBuilder(Label.EXTERNAL_PACKAGE_IDENTIFIER, "TESTING")
            .setFilename(myPkgPath)
            .setMakeEnv(new MakeEnvironment.Builder());

    Map<String, Object> attributeValues = new HashMap<>();
    attributeValues.put("name", "foo");
    attributeValues.put("alwayslink", true);

    RuleClass ruleClass = provider.getRuleClassMap().get("cc_library");
    try {
      RuleFactory.createAndAddRule(
          pkgBuilder,
          ruleClass,
          new BuildLangTypedAttributeValuesMap(attributeValues),
          new Reporter(),
          /*ast=*/ null,
          Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
          /*env=*/ null,
          new AttributeContainer(ruleClass));
      fail();
    } catch (RuleFactory.InvalidRuleException e) {
      assertThat(e.getMessage()).contains("cannot be in the WORKSPACE file");
    }
  }

  private void assertAttr(RuleClass ruleClass, String attrName, Type<?> type) throws Exception {
    assertTrue(
        "Rule class '"
            + ruleClass.getName()
            + "' should have attribute '"
            + attrName
            + "' of type '"
            + type
            + "'",
        ruleClass.hasAttr(attrName, type));
  }

  @Test
  public void testOutputFileNotEqualDot() throws Exception {
    Path myPkgPath = scratch.resolve("/foo");
    Package.Builder pkgBuilder =
        packageFactory.newPackageBuilder(PackageIdentifier.createInMainRepo("mypkg"), "TESTING")
            .setFilename(myPkgPath)
            .setMakeEnv(new MakeEnvironment.Builder());

    Map<String, Object> attributeValues = new HashMap<>();
    attributeValues.put("outs", Lists.newArrayList("."));
    attributeValues.put("name", "some");
    RuleClass ruleClass = provider.getRuleClassMap().get("genrule");
    try {
      RuleFactory.createAndAddRule(
          pkgBuilder,
          ruleClass,
          new BuildLangTypedAttributeValuesMap(attributeValues),
          new Reporter(),
          /*ast=*/ null,
          Location.fromFileAndOffsets(myPkgPath.asFragment(), 42, 42),
          /*env=*/ null,
          new AttributeContainer(ruleClass));
      fail();
    } catch (RuleFactory.InvalidRuleException e) {
      assertTrue(e.getMessage(), e.getMessage().contains("output file name can't be equal '.'"));
    }
  }

  /**
   * Tests mandatory attribute definitions for test rules.
   */
  // TODO(ulfjack): Remove this check when we switch over to the builder
  // pattern, which will always guarantee that these attributes are present.
  @Test
  public void testTestRules() throws Exception {
    Path myPkgPath = scratch.resolve("/foo/workspace/mypkg/BUILD");
    Package pkg =
        packageFactory.newPackageBuilder(PackageIdentifier.createInMainRepo("mypkg"), "TESTING")
            .setFilename(myPkgPath)
            .setMakeEnv(new MakeEnvironment.Builder())
            .build();

    for (String name : ruleFactory.getRuleClassNames()) {
      // Create rule instance directly so we'll avoid mandatory attribute check yet will be able
      // to use TargetUtils.isTestRule() method to identify test rules.
      RuleClass ruleClass = ruleFactory.getRuleClass(name);
      Rule rule =
          new Rule(
              pkg,
              pkg.createLabel("myrule"),
              ruleClass,
              Location.fromFile(myPkgPath),
              new AttributeContainer(ruleClass));
      if (TargetUtils.isTestRule(rule)) {
        assertAttr(ruleClass, "tags", Type.STRING_LIST);
        assertAttr(ruleClass, "size", Type.STRING);
        assertAttr(ruleClass, "flaky", Type.BOOLEAN);
        assertAttr(ruleClass, "shard_count", Type.INTEGER);
        assertAttr(ruleClass, "local", Type.BOOLEAN);
      }
    }
  }
}
