// Copyright 2018 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.android;

import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.CoreMatchers.containsString;
import static org.junit.Assert.assertNotNull;

import com.android.builder.core.VariantType;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.devtools.build.android.AarGeneratorAction.AarGeneratorOptions;
import com.google.devtools.build.zip.ZipReader;
import com.google.devtools.common.options.OptionsParser;
import com.google.devtools.common.options.OptionsParsingException;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link AarGeneratorAction}. */
@RunWith(JUnit4.class)
public class AarGeneratorActionTest {

  private static class AarData {
    /** Templates for resource files generation. */
    enum ResourceType {
      VALUE {
        @Override public String create(String... lines) {
          return String.format(
              "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>%s</resources>",
              Joiner.on("\n").join(lines));
        }
      },
      LAYOUT {
        @Override public String create(String... lines) {
          return String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
              + "<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\""
              + " android:layout_width=\"fill_parent\""
              + " android:layout_height=\"fill_parent\">%s</LinearLayout>",
              Joiner.on("\n").join(lines));
        }
      },
      UNFORMATTED {
        @Override public String create(String... lines) {
          return String.format(Joiner.on("\n").join(lines));
        }
      };

      public abstract String create(String... lines);
    }

    private static class Builder {

      private final Path root;
      private final Path assetDir;
      private final Path resourceDir;
      private Path manifest;
      private Path rtxt;
      private Path classes;
      private Map<Path, String> filesToWrite = new HashMap<>();
      private Map<String, String> classesToWrite = new HashMap<>();
      private ImmutableList.Builder<Path> proguardSpecs = ImmutableList.builder();
      private boolean withEmptyRes = false;
      private boolean withEmptyAssets = false;

      public Builder(Path root) {
        this(root, "res", "assets");
      }

      public Builder(Path root, String resourceRoot, String assetRoot) {
        this.root = root;
        assetDir = root.resolve(assetRoot);
        resourceDir = root.resolve(resourceRoot);
        manifest = root.resolve("fake-manifest-path");
        rtxt = root.resolve("fake-rtxt-path");
        classes = root.resolve("fake-classes-path");
      }

      public Builder addResource(String path, ResourceType template, String... lines) {
        filesToWrite.put(resourceDir.resolve(path), template.create(lines));
        return this;
      }

      public Builder withEmptyResources(boolean isEmpty) {
        this.withEmptyRes = isEmpty;
        return this;
      }

      public Builder addAsset(String path, String... lines) {
        filesToWrite.put(assetDir.resolve(path), Joiner.on("\n").join(lines));
        return this;
      }

      public Builder withEmptyAssets(boolean isEmpty) {
        this.withEmptyAssets = isEmpty;
        return this;
      }

      public Builder createManifest(String path, String manifestPackage, String... lines) {
        this.manifest = root.resolve(path);
        filesToWrite.put(manifest, String.format("<?xml version=\"1.0\" encoding=\"utf-8\"?>"
            + "<manifest xmlns:android='http://schemas.android.com/apk/res/android' package='%s'>"
            + "%s</manifest>", manifestPackage, Joiner.on("\n").join(lines)));
        return this;
      }

      public Builder createRtxt(String path, String... lines) {
        this.rtxt = root.resolve(path);
        filesToWrite.put(rtxt, String.format("%s", Joiner.on("\n").join(lines)));
        return this;
      }

      public Builder createClassesJar(String path) {
        this.classes = root.resolve(path);
        classesToWrite.put("META-INF/MANIFEST.MF", "Manifest-Version: 1.0\n");
        return this;
      }

      public Builder addClassesFile(String filePackage, String filename, String... lines) {
        classesToWrite.put(filePackage.replace(".", "/") + "/" + filename,
            String.format("%s", Joiner.on("\n").join(lines)));
        return this;
      }

      public Builder addProguardSpec(String path, String... lines) {
        Path proguardSpecPath = root.resolve(path);
        proguardSpecs.add(proguardSpecPath);
        filesToWrite.put(proguardSpecPath, String.format("%s", Joiner.on("\n").join(lines)));
        return this;
      }

      public AarData build() throws IOException {
        writeFiles();
        return new AarData(buildMerged(), manifest, rtxt, classes, proguardSpecs.build());
      }

      private MergedAndroidData buildMerged() {
        return new MergedAndroidData(
            resourceDir,
            assetDir,
            manifest);
      }

      private void writeFiles() throws IOException {
        assertNotNull("A manifest is required.", manifest);
        assertNotNull("A resource file is required.", rtxt);
        assertNotNull("A classes jar is required.", classes);
        if (withEmptyRes) {
          Files.createDirectories(resourceDir);
        }
        if (withEmptyAssets) {
          Files.createDirectories(assetDir);
        }
        for (Entry<Path, String> entry : filesToWrite.entrySet()) {
          Path file = entry.getKey();
          // only write files in assets if assets has not been set to empty and same for resources
          if (!((file.startsWith(assetDir) && withEmptyAssets)
              || (file.startsWith(resourceDir) && withEmptyRes))) {
            Files.createDirectories(file.getParent());
            Files.write(file, entry.getValue().getBytes(StandardCharsets.UTF_8));
            assertThat(Files.exists(file)).isTrue();
          }
        }
        if (!classesToWrite.isEmpty()) {
          writeClassesJar();
        }
      }

      private void writeClassesJar() throws IOException {
        final ZipOutputStream zout = new ZipOutputStream(new FileOutputStream(classes.toFile()));

        for (Entry<String, String> file : classesToWrite.entrySet()) {
          ZipEntry entry = new ZipEntry(file.getKey());
          zout.putNextEntry(entry);
          zout.write(file.getValue().getBytes(UTF_8));
          zout.closeEntry();
        }

        zout.close();

        classes.toFile().setLastModified(AarGeneratorAction.DEFAULT_TIMESTAMP);
      }
    }

    final MergedAndroidData data;
    final Path manifest;
    final Path rtxt;
    final Path classes;
    final ImmutableList<Path> proguardSpecs;

    private AarData(
        MergedAndroidData data,
        Path manifest,
        Path rtxt,
        Path classes,
        ImmutableList<Path> proguardSpecs) {
      this.data = data;
      this.manifest = manifest;
      this.rtxt = rtxt;
      this.classes = classes;
      this.proguardSpecs = proguardSpecs;
    }
  }

  /**
   * Operation to perform on a file.
   */
  private interface FileOperation {
    /**
     * Performs the operation on a file, given its name, modificationTime and contents.
     */
    void perform(String name, long modificationTime, String contents);
  }

  /**
   * Runs a {@link FileOperation} on every entry in a zip file.
   *
   * @param zip {@link Path} of the zip file to traverse.
   * @param operation {@link FileOperation} to be run on every entry of the zip file.
   * @throws IOException if there is an error reading the zip file.
   */
  private void traverseZipFile(Path zip, FileOperation operation) throws IOException {
    ZipInputStream zis = new ZipInputStream(Files.newInputStream(zip));
    ZipEntry z = zis.getNextEntry();
    while (z != null) {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      byte[] buffer = new byte[1024];
      for (int count = 0; count != -1; count = zis.read(buffer)) {
        baos.write(buffer);
      }
      // Replace Windows path separators so that test cases are consistent across platforms.
      String name = z.getName().replace('\\', '/');
      operation.perform(
          name, z.getTime(), new String(baos.toByteArray(), StandardCharsets.UTF_8));
      z = zis.getNextEntry();
    }
  }

  private Set<String> getZipEntries(Path zip) throws IOException {
    final Set<String> zipEntries = new HashSet<>();
    traverseZipFile(zip, new FileOperation() {
      @Override public void perform(String name, long modificationTime, String contents) {
        zipEntries.add(name);
      }
    });
    return zipEntries;
  }

  private Set<Long> getZipEntryTimestamps(Path zip) throws IOException {
    final Set<Long> timestamps = new HashSet<>();
    traverseZipFile(zip, new FileOperation() {
      @Override public void perform(String name, long modificationTime, String contents) {
        timestamps.add(modificationTime);
      }
    });
    return timestamps;
  }

  private Path tempDir;

  @Rule public ExpectedException thrown = ExpectedException.none();

  @Before public void setUp() throws IOException {
    tempDir = Files.createTempDirectory(toString());
    tempDir.toFile().deleteOnExit();

  }

  @Test public void testCheckFlags() throws IOException, OptionsParsingException {
    Path manifest = tempDir.resolve("AndroidManifest.xml");
    Files.createFile(manifest);
    Path rtxt = tempDir.resolve("R.txt");
    Files.createFile(rtxt);
    Path classes = tempDir.resolve("classes.jar");
    Files.createFile(classes);

    String[] args = new String[] {"--manifest", manifest.toString(), "--rtxt", rtxt.toString(),
        "--classes", classes.toString()};
    OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class);
    optionsParser.parse(args);
    AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class);
    AarGeneratorAction.checkFlags(options);
  }

  @Test public void testCheckFlags_MissingClasses() throws IOException, OptionsParsingException {
    Path manifest = tempDir.resolve("AndroidManifest.xml");
    Files.createFile(manifest);
    Path rtxt = tempDir.resolve("R.txt");
    Files.createFile(rtxt);

    String[] args = new String[] {"--manifest", manifest.toString(), "--rtxt", rtxt.toString()};
    OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class);
    optionsParser.parse(args);
    AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class);
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("classes must be specified. Building an .aar without"
          + " classes is unsupported.");
    AarGeneratorAction.checkFlags(options);
  }

  @Test public void testCheckFlags_MissingMultiple() throws IOException, OptionsParsingException {
    Path manifest = tempDir.resolve("AndroidManifest.xml");
    Files.createFile(manifest);
    String[] args = new String[] {"--manifest", manifest.toString()};
    OptionsParser optionsParser = OptionsParser.newOptionsParser(AarGeneratorOptions.class);
    optionsParser.parse(args);
    AarGeneratorOptions options = optionsParser.getOptions(AarGeneratorOptions.class);
    thrown.expect(IllegalArgumentException.class);
    thrown.expectMessage("rtxt, classes must be specified. Building an .aar without"
          + " rtxt, classes is unsupported.");
    AarGeneratorAction.checkFlags(options);
  }

  @Test public void testWriteAar() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testWriteAar_DefaultTimestamps() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);

    assertThat(getZipEntryTimestamps(aar)).containsExactly(AarGeneratorAction.DEFAULT_TIMESTAMP);
    assertThat(aar.toFile().lastModified()).isEqualTo(AarGeneratorAction.DEFAULT_TIMESTAMP);
  }

  @Test public void testAssetResourceSubdirs() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"), "xyz", "assets")
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);

    // verify aar archive
    Set<String> zipEntries = getZipEntries(aar);
    assertThat(zipEntries).contains("res/");
    assertThat(zipEntries).contains("assets/");
  }

  @Test public void testMissingManifest() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    thrown.expect(IOException.class);
    thrown.expectMessage(containsString("fake-manifest-path"));
    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testMissingRtxt() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    thrown.expect(IOException.class);
    thrown.expectMessage(containsString("fake-rtxt-path"));
    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testMissingClasses() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .build();

    thrown.expect(IOException.class);
    thrown.expectMessage(containsString("fake-classes-path"));
    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testMissingResources() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    thrown.expect(IOException.class);
    thrown.expectMessage(containsString("res"));
    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testEmptyResources() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .withEmptyResources(true)
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .addAsset("some/other/ft/data.txt", "bar")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testMissingAssets() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testEmptyAssets() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id_name\" type=\"id\"/>")
        .withEmptyAssets(true)
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    AarGeneratorAction.writeAar(aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
  }

  @Test public void testFullIntegration() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData = new AarData.Builder(tempDir.resolve("data"))
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo", "")
        .createRtxt("R.txt",
            "int string app_name 0x7f050001",
            "int string hello_world 0x7f050002")
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id\" type=\"id\"/>")
        .addResource("layout/layout.xml",
            AarData.ResourceType.LAYOUT,
            "<TextView android:id=\"@+id/text2\""
                + " android:layout_width=\"wrap_content\""
                + " android:layout_height=\"wrap_content\""
                + " android:text=\"Hello, I am a TextView\" />")
        .addAsset("some/other/ft/data.txt",
            "foo")
        .createClassesJar("classes.jar")
        .addClassesFile("com.google.android.apps.foo", "Test.class", "test file contents")
        .build();

    MergedAndroidData md1 = new AarData.Builder(tempDir.resolve("d1"))
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id\" type=\"id\"/>")
        .addResource("layout/foo.xml",
            AarData.ResourceType.LAYOUT,
            "<TextView android:id=\"@+id/text\""
                + " android:layout_width=\"wrap_content\""
                + " android:layout_height=\"wrap_content\""
                + " android:text=\"Hello, I am a TextView\" />")
        .addAsset("some/other/ft/data1.txt",
            "bar")
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
        .build().data;

    MergedAndroidData md2 = new AarData.Builder(tempDir.resolve("d2"))
        .addResource("values/ids.xml",
            AarData.ResourceType.VALUE,
            "<item name=\"id2\" type=\"id\"/>")
        .addResource("layout/bar.xml",
            AarData.ResourceType.LAYOUT,
            "<TextView android:id=\"@+id/textbar\""
                + " android:layout_width=\"wrap_content\""
                + " android:layout_height=\"wrap_content\""
                + " android:text=\"Hello, I am a TextView\" />")
        .addResource("drawable-mdpi/icon.png",
            AarData.ResourceType.UNFORMATTED,
            "Thttpt.")
        .addResource("drawable-xxxhdpi/icon.png",
            AarData.ResourceType.UNFORMATTED,
            "Double Thttpt.")
        .addAsset("some/other/ft/data2.txt",
            "foo")
        .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d2", "")
        .build().data;

    UnvalidatedAndroidData primary = new UnvalidatedAndroidData(
        ImmutableList.of(aarData.data.getResourceDir()),
        ImmutableList.of(aarData.data.getAssetDir()),
        aarData.data.getManifest());

    DependencyAndroidData d1 =
        new DependencyAndroidData(
            ImmutableList.of(md1.getResourceDir()),
            ImmutableList.of(md1.getAssetDir()),
            md1.getManifest(),
            null,
            null,
            null);

    DependencyAndroidData d2 =
        new DependencyAndroidData(
            ImmutableList.of(md2.getResourceDir()),
            ImmutableList.of(md2.getAssetDir()),
            md2.getManifest(),
            null,
            null,
            null);

    Path working = tempDir;

    Path resourcesOut = working.resolve("resources");
    Path assetsOut = working.resolve("assets");

    MergedAndroidData mergedData =
        AndroidResourceMerger.mergeData(
            primary,
            ImmutableList.of(d1, d2),
            ImmutableList.<DependencyAndroidData>of(),
            resourcesOut,
            assetsOut,
            null,
            VariantType.LIBRARY,
            null,
            /* filteredResources= */ ImmutableList.of(),
            true);

    AarGeneratorAction.writeAar(
        aar, mergedData, aarData.manifest, aarData.rtxt, aarData.classes, aarData.proguardSpecs);

    // verify aar archive
    Set<String> zipEntries = getZipEntries(aar);
    assertThat(zipEntries).containsExactly(
        "AndroidManifest.xml",
        "R.txt",
        "classes.jar",
        "res/",
        "res/values/",
        "res/values/values.xml",
        "res/layout/",
        "res/layout/layout.xml",
        "res/layout/foo.xml",
        "res/layout/bar.xml",
        "res/drawable-mdpi-v4/",
        "res/drawable-mdpi-v4/icon.png",
        "res/drawable-xxxhdpi-v4/",
        "res/drawable-xxxhdpi-v4/icon.png",
        "assets/",
        "assets/some/",
        "assets/some/other/",
        "assets/some/other/ft/",
        "assets/some/other/ft/data.txt",
        "assets/some/other/ft/data1.txt",
        "assets/some/other/ft/data2.txt");
  }

  @Test public void testProguardSpecs() throws Exception {
    Path aar = tempDir.resolve("foo.aar");
    AarData aarData =
        new AarData.Builder(tempDir.resolve("data"))
            .createManifest("AndroidManifest.xml", "com.google.android.apps.foo.d1", "")
            .createRtxt("R.txt", "")
            .withEmptyResources(true)
            .withEmptyAssets(true)
            .createClassesJar("classes.jar")
            .addProguardSpec("spec1", "foo", "bar")
            .addProguardSpec("spec2", "baz")
            .build();

    AarGeneratorAction.writeAar(
        aar,
        aarData.data,
        aarData.manifest,
        aarData.rtxt,
        aarData.classes,
        aarData.proguardSpecs);
    Set<String> zipEntries = getZipEntries(aar);
    assertThat(zipEntries).contains("proguard.txt");
    ZipReader aarReader = new ZipReader(aar.toFile());
    List<String> proguardTxtContents =
        new BufferedReader(
                new InputStreamReader(
                    aarReader.getInputStream(aarReader.getEntry("proguard.txt")),
                    StandardCharsets.UTF_8))
            .lines()
            .collect(Collectors.toList());
    assertThat(proguardTxtContents).containsExactly("foo", "bar", "baz").inOrder();
  }
}
