/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * 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.
 */

#include "java/ProguardRules.h"
#include "link/Linkers.h"

#include "io/StringStream.h"
#include "test/Test.h"

using ::aapt::io::StringOutputStream;
using ::android::ConfigDescription;
using ::testing::HasSubstr;
using ::testing::Not;

namespace aapt {

std::string GetKeepSetString(const proguard::KeepSet& set, bool minimal_rules) {
  std::string out;
  StringOutputStream sout(&out);
  proguard::WriteKeepSet(set, &sout, minimal_rules);
  sout.Flush();
  return out;
}

TEST(ProguardRulesTest, ManifestRuleDefaultConstructorOnly) {
  std::unique_ptr<xml::XmlResource> manifest = test::BuildXmlDom(R"(
      <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application
            android:appComponentFactory="com.foo.BarAppComponentFactory"
            android:backupAgent="com.foo.BarBackupAgent"
            android:name="com.foo.BarApplication"
            android:zygotePreloadName="com.foo.BarZygotePreload"
            >
          <activity android:name="com.foo.BarActivity"/>
          <service android:name="com.foo.BarService"/>
          <receiver android:name="com.foo.BarReceiver"/>
          <provider android:name="com.foo.BarProvider"/>
        </application>
        <instrumentation android:name="com.foo.BarInstrumentation"/>
      </manifest>)");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRulesForManifest(manifest.get(), &set, false));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarAppComponentFactory { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarBackupAgent { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarApplication { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarActivity { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarService { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarReceiver { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarProvider { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarInstrumentation { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarZygotePreload { <init>(); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarAppComponentFactory { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarBackupAgent { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarApplication { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarActivity { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarService { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarReceiver { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarProvider { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarInstrumentation { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.BarZygotePreload { <init>(); }"));
}

TEST(ProguardRulesTest, FragmentNameRuleIsEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <fragment xmlns:android="http://schemas.android.com/apk/res/android"
          android:name="com.foo.Bar"/>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }"));
}

TEST(ProguardRulesTest, FragmentClassRuleIsEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout =
      test::BuildXmlDom(R"(<fragment class="com.foo.Bar"/>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }"));
}

TEST(ProguardRulesTest, FragmentNameAndClassRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <fragment xmlns:android="http://schemas.android.com/apk/res/android"
          android:name="com.foo.Baz"
          class="com.foo.Bar"/>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(); }"));
}

TEST(ProguardRulesTest, NavigationFragmentNameAndClassRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder()
      .SetCompilationPackage("com.base").Build();
  std::unique_ptr<xml::XmlResource> navigation = test::BuildXmlDom(R"(
      <navigation
          xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto">
          <custom android:id="@id/foo"
              android:name="com.package.Foo"/>
          <fragment android:id="@id/bar"
              android:name="com.package.Bar">
              <nested android:id="@id/nested"
                  android:name=".Nested"/>
          </fragment>
      </navigation>
  )");

  navigation->file.name = test::ParseNameOrDie("navigation/graph.xml");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), navigation.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.package.Foo { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.package.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.base.Nested { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-keep class com.package.Foo { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.package.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.base.Nested { <init>(...); }"));
}

TEST(ProguardRulesTest, CustomViewRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android">
        <com.foo.Bar />
      </View>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
      "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
}

TEST(ProguardRulesTest, IncludedLayoutRulesAreConditional) {
  std::unique_ptr<xml::XmlResource> bar_layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android">
        <com.foo.Bar />
      </View>)");
  bar_layout->file.name = test::ParseNameOrDie("com.foo:layout/bar");

  ResourceTable table;
  StdErrDiagnostics errDiagnostics;
  table.AddResource(bar_layout->file.name, ConfigDescription::DefaultConfig(), "",
                    util::make_unique<FileReference>(), &errDiagnostics);

  std::unique_ptr<IAaptContext> context =
      test::ContextBuilder()
          .SetCompilationPackage("com.foo")
          .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(&table))
          .Build();

  std::unique_ptr<xml::XmlResource> foo_layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android">
        <include layout="@layout/bar" />
      </View>)");
  foo_layout->file.name = test::ParseNameOrDie("com.foo:layout/foo");

  XmlReferenceLinker xml_linker;
  ASSERT_TRUE(xml_linker.Consume(context.get(), bar_layout.get()));
  ASSERT_TRUE(xml_linker.Consume(context.get(), foo_layout.get()));

  proguard::KeepSet set = proguard::KeepSet(true);
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), bar_layout.get(), &set));
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), foo_layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("int foo"));
  EXPECT_THAT(actual, HasSubstr("int bar"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
  EXPECT_THAT(actual, HasSubstr(
    "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
  EXPECT_THAT(actual, HasSubstr("int foo"));
  EXPECT_THAT(actual, HasSubstr("int bar"));
}

TEST(ProguardRulesTest, AliasedLayoutRulesAreConditional) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android">
        <com.foo.Bar />
      </View>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set = proguard::KeepSet(true);
  set.AddReference({test::ParseNameOrDie("layout/bar"), {}}, layout->file.name);
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr(
      "-keep class com.foo.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
  EXPECT_THAT(actual, HasSubstr("int foo"));
  EXPECT_THAT(actual, HasSubstr("int bar"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
    "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
  EXPECT_THAT(actual, HasSubstr("-if class **.R$layout"));
  EXPECT_THAT(actual, HasSubstr("int foo"));
  EXPECT_THAT(actual, HasSubstr("int bar"));
}

TEST(ProguardRulesTest, NonLayoutReferencesAreUnconditional) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android">
        <com.foo.Bar />
      </View>)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set = proguard::KeepSet(true);
  set.AddReference({test::ParseNameOrDie("style/MyStyle"), {}}, layout->file.name);
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, Not(HasSubstr("-if")));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, Not(HasSubstr("-if")));
  EXPECT_THAT(actual, HasSubstr(
    "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
}

TEST(ProguardRulesTest, ViewOnClickRuleIsEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> layout = test::BuildXmlDom(R"(
      <View xmlns:android="http://schemas.android.com/apk/res/android"
          android:onClick="bar_method" />)");
  layout->file.name = test::ParseNameOrDie("layout/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), layout.get(), &set));

  std::string actual = GetKeepSetString(set,  /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr(
      "-keepclassmembers class * { *** bar_method(android.view.View); }"));

  actual = GetKeepSetString(set,  /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
    "-keepclassmembers class * { *** bar_method(android.view.View); }"));
}

TEST(ProguardRulesTest, MenuRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> menu = test::BuildXmlDom(R"(
      <menu xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:onClick="on_click"
            android:actionViewClass="com.foo.Bar"
            android:actionProviderClass="com.foo.Baz"
            android:name="com.foo.Bat" />
      </menu>)");
  menu->file.name = test::ParseNameOrDie("menu/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), menu.get(), &set));

  std::string actual = GetKeepSetString(set,  /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr(
    "-keepclassmembers class * { *** on_click(android.view.MenuItem); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(...); }"));
  EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat")));

  actual = GetKeepSetString(set,  /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
    "-keepclassmembers class * { *** on_click(android.view.MenuItem); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(android.content.Context); }"));
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Baz { <init>(android.content.Context); }"));
  EXPECT_THAT(actual, Not(HasSubstr("com.foo.Bat")));
}

TEST(ProguardRulesTest, TransitionPathMotionRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> transition = test::BuildXmlDom(R"(
      <changeBounds>
        <pathMotion class="com.foo.Bar"/>
      </changeBounds>)");
  transition->file.name = test::ParseNameOrDie("transition/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transition.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
    "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
}

TEST(ProguardRulesTest, TransitionRulesAreEmitted) {
  std::unique_ptr<IAaptContext> context = test::ContextBuilder().Build();
  std::unique_ptr<xml::XmlResource> transitionSet = test::BuildXmlDom(R"(
      <transitionSet>
        <transition class="com.foo.Bar"/>
      </transitionSet>)");
  transitionSet->file.name = test::ParseNameOrDie("transition/foo");

  proguard::KeepSet set;
  ASSERT_TRUE(proguard::CollectProguardRules(context.get(), transitionSet.get(), &set));

  std::string actual = GetKeepSetString(set, /** minimal_rules */ false);
  EXPECT_THAT(actual, HasSubstr("-keep class com.foo.Bar { <init>(...); }"));

  actual = GetKeepSetString(set, /** minimal_rules */ true);
  EXPECT_THAT(actual, HasSubstr(
    "-keep class com.foo.Bar { <init>(android.content.Context, android.util.AttributeSet); }"));
}

}  // namespace aapt