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

import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;

import com.android.ide.common.internal.PngCruncher;
import com.android.ide.common.internal.PngException;
import com.google.common.collect.ImmutableMap;
import com.google.common.jimfs.Jimfs;
import com.google.common.truth.Subject;
import com.google.common.util.concurrent.MoreExecutors;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for the AndroidDataWriter. */
@RunWith(JUnit4.class)
public class AndroidDataWriterTest {

  private static final String START_RESOURCES =
      new String(AndroidDataWriter.PRELUDE) + "<resources";
  private static final String END_RESOURCES = new String(AndroidDataWriter.END_RESOURCES);

  private FileSystem fs;

  @Before
  public void createCleanEnvironment() {
    fs = Jimfs.newFileSystem();
  }

  @Test
  public void writeResourceFile() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    String drawable = "drawable/menu.gif";
    String layout = "layout/foo.xml";
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(layout, AndroidDataBuilder.ResourceType.LAYOUT, "")
            .addResourceBinary(drawable, Files.createFile(fs.getPath("menu.gif")))
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(drawable)).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(layout)).exists();
  }

  @Test
  public void writePngInRawAndNotInRaw() throws Exception {
    Path tmpDir = Files.createTempDirectory(this.toString());
    final Path target = tmpDir.resolve("target");
    final Path source = tmpDir.resolve("source");
    final String drawable = "drawable/crunch.png";
    final String raw = "raw/nocrunch.png";
    final String layout = "layout/foo.xml";
    AndroidDataWriter mergedDataWriter =
        AndroidDataWriter.createWith(
            target,
            target.resolve("res"),
            target.resolve("assets"),
            new PngCruncher() {
              @Override
              public int start() {
                return 0;
              }

              @Override
              public void end(int key) throws InterruptedException {
              }

              @Override
              public void crunchPng(int key, File from, File to)
                  throws PngException {
                assertThat(from.toString()).doesNotContain(raw);
                try {
                  Files.copy(from.toPath(), to.toPath());
                } catch (IOException e) {
                  throw new PngException(e);
                }
              }
            },
            MoreExecutors.newDirectExecutorService());
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(layout, AndroidDataBuilder.ResourceType.LAYOUT, "")
            .addResourceBinary(drawable, Files.createFile(tmpDir.resolve("crunch.png")))
            .addResourceBinary(raw, Files.createFile(tmpDir.resolve("nocrunch.png")))
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(drawable)).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(raw)).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(layout)).exists();
  }

  @Test
  public void writeNinepatchResourceFile() throws Exception {
    Path tmpDir = Files.createTempDirectory(this.toString());
    Path target = tmpDir.resolve("target");
    Path source = tmpDir.resolve("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    String drawable = "drawable-hdpi-v4/seven_eight.9.png";
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResourceBinary(drawable, Files.createFile(fs.getPath("seven_eight.9.png")))
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve(drawable)).exists();
  }

  @Test
  public void writeResourceXml() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(
                "values/ids.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<item name=\"id1\" type=\"id\"/>",
                "<item name=\"id\" type=\"id\"/>")
            .addResource(
                "values/stubs.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<item name=\"walrus\" type=\"drawable\"/>")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + ">",
            "<!-- " + fs.getPath("source/res/values/stubs.xml") + " --><eat-comment/>",
            "<item name='walrus' type='drawable'/>",
            "<!-- " + fs.getPath("source/res/values/ids.xml") + " --><eat-comment/>",
            "<item name='id' type='id'/>",
            "<item name='id1' type='id'/>",
            END_RESOURCES);
  }

  @Test
  public void writeResourceXmlWithQualfiers() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(
                "values/ids.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<item name=\"id1\" type=\"id\"/>")
            .addResource(
                "values-en/ids.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<item name=\"id1\" type=\"id\"/>")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + ">",
            "<!-- "
                + fs.getPath("source/res/values/ids.xml")
                + " --><eat-comment/><item name='id1' type='id'/>",
            END_RESOURCES);
    assertAbout(paths).that(actual.getResourceDir().resolve("values-en/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values-en/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + ">",
            "<!-- "
                + fs.getPath("source/res/values-en/ids.xml")
                + " --><eat-comment/><item name='id1' type='id'/>",
            END_RESOURCES);
  }

  @Test
  public void writePublicResourceSameNameDifferentType() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(
                "values/integers.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<integer name=\"foo\">12345</integer>",
                "<public name=\"foo\" type=\"integer\" id=\"0x7f040000\"/>",
                "<integer name=\"zoo\">54321</integer>",
                "<public name=\"zoo\" type=\"integer\" />")
            .addResource(
                "values/strings.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<string name=\"foo\">meow</string>",
                "<public name=\"foo\" type=\"string\" id=\"0x7f050000\"/>")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + ">",
            "<!-- " + fs.getPath("source/res/values/integers.xml") + " --><eat-comment/>",
            "<integer name='foo'>12345</integer>",
            "<integer name='zoo'>54321</integer>",
            "<!-- " + fs.getPath("source/res/values/strings.xml") + " --><eat-comment/>",
            "<string name='foo'>meow</string>",
            "<!-- " + fs.getPath("source/res/values/integers.xml") + " --><eat-comment/>",
            "<public name='foo' type='integer' id='0x7f040000'/>",
            "<public name='foo' type='string' id='0x7f050000'/>",
            "<public name='zoo' type='integer' />",
            END_RESOURCES);
  }

  @Test
  public void writeWithIDDuplicates() throws Exception {
    // We parse IDs from layout, etc. XML. We can include it in the merged values.xml redundantly
    // (redundant because we also give aapt the original layout xml, which it can parse for IDs
    // too), but make sure we don't accidentally put multiple copies in the merged values.xml file.
    // Otherwise, aapt will throw an error if there are duplicates in the same values.xml file.
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addResource(
                "layout/some_layout.xml",
                AndroidDataBuilder.ResourceType.LAYOUT,
                "<TextView android:id=\"@+id/MyTextView\"",
                "          android:text=\"@string/walrus\"",
                "          android:layout_above=\"@+id/AnotherTextView\"",
                "          android:layout_width=\"wrap_content\"",
                "          android:layout_height=\"wrap_content\" />",
                // Test redundantly having a "+id/MyTextView" in a different attribute.
                "<TextView android:id=\"@id/AnotherTextView\"",
                "          android:text=\"@string/walrus\"",
                "          android:layout_below=\"@+id/MyTextView\"",
                "          android:layout_width=\"wrap_content\"",
                "          android:layout_height=\"wrap_content\" />")
            // Test what happens if a user accidentally uses the same ID in multiple layouts too.
            .addResource(
                "layout/another_layout.xml",
                AndroidDataBuilder.ResourceType.LAYOUT,
                "<TextView android:id=\"@+id/MyTextView\"",
                "          android:text=\"@string/walrus\"",
                "          android:layout_width=\"wrap_content\"",
                "          android:layout_height=\"wrap_content\" />")
            // Also check what happens if a value XML file also contains the same ID.
            .addResource(
                "values/ids.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<item name=\"MyTextView\" type=\"id\"/>",
                "<item name=\"OtherId\" type=\"id\"/>")
            .addResource(
                "values/strings.xml",
                AndroidDataBuilder.ResourceType.VALUE,
                "<string name=\"walrus\">I has a bucket</string>")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("layout/some_layout.xml")).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + ">",
            "<!-- " + fs.getPath("source/res/values/ids.xml") + " --><eat-comment/>",
            "<item name='MyTextView' type='id'/>",
            "<item name='OtherId' type='id'/>",
            "<!-- " + fs.getPath("source/res/values/strings.xml") + " --><eat-comment/>",
            "<string name='walrus'>I has a bucket</string>",
            END_RESOURCES);
  }

  @Test
  public void writeResourceXmlWithAttributes() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addValuesWithAttributes(
                "values/ids.xml",
                ImmutableMap.of("foo", "fooVal", "bar", "barVal"),
                "<item name=\"id1\" type=\"id\"/>",
                "<item name=\"id\" type=\"id\"/>")
            .addValuesWithAttributes(
                "values/stubs.xml",
                ImmutableMap.of("baz", "bazVal"),
                "<item name=\"walrus\" type=\"drawable\"/>")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getResourceDir().resolve("values/values.xml")).exists();
    assertAbout(paths)
        .that(actual.getResourceDir().resolve("values/values.xml"))
        .xmlContentsIsEqualTo(
            START_RESOURCES + " foo=\"fooVal\" bar=\"barVal\" baz=\"bazVal\">",
            "<!-- " + fs.getPath("source/res/values/stubs.xml") + " --><eat-comment/>",
            "<item name='walrus' type='drawable'/>",
            "<!-- " + fs.getPath("source/res/values/ids.xml") + " --><eat-comment/>",
            "<item name='id' type='id'/>",
            "<item name='id1' type='id'/>",
            END_RESOURCES);
  }

  @Test
  public void writeAssetFile() throws Exception {
    Path target = fs.getPath("target");
    Path source = fs.getPath("source");
    AndroidDataWriter mergedDataWriter = AndroidDataWriter.createWithDefaults(target);
    String asset = "hunting/of/the/boojum";
    ParsedAndroidData direct =
        AndroidDataBuilder.of(source)
            .addAsset(asset, "not a snark!")
            .createManifest("AndroidManifest.xml", "com.carroll.lewis", "")
            .buildParsed();
    MergedAndroidData actual =
        UnwrittenMergedAndroidData.of(
                source.resolve("AndroidManifest.xml"), direct, ParsedAndroidDataBuilder.empty())
            .write(mergedDataWriter);

    assertAbout(paths).that(actual.getManifest()).exists();
    assertAbout(paths).that(actual.getAssetDir().resolve(asset)).exists();
  }

  private static final Subject.Factory<PathsSubject, Path> paths = PathsSubject::new;
}
