// 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.assertThat;
import static com.google.devtools.build.android.DensitySpecificManifestProcessor.PLAY_STORE_SUPPORTED_DENSITIES;
import static com.google.devtools.build.android.DensitySpecificManifestProcessor.SCREEN_SIZES;
import static org.junit.Assert.assertThrows;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.jimfs.Jimfs;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/** Tests for {@link DensitySpecificManifestProcessor}. */
@RunWith(JUnit4.class)
public class DensitySpecificManifestProcessorTest {

  private FileSystem fs;
  private Path tmp;

  @Test public void testNoDensities() throws Exception {
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(ImmutableList.<String>of(),
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isEqualTo(manifest);
  }

  @Test public void testSingleDensity() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("xhdpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkModification(modified, densities);
  }
  
  @Test public void test280Density() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("280dpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkModification(modified, densities);
  }

  @Test public void testMultipleDensities() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("xhdpi", "xxhdpi", "560dpi", "xxxhdpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkModification(modified, densities);
  }

  @Test public void omitCompatibleScreensIfDensityUnsupported() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("xhdpi", "340dpi", "xxhdpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkCompatibleScreensOmitted(modified);
  }

  @Test public void testExistingCompatibleScreens() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("xhdpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "<compatible-screens>",
        "</compatible-screens>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkModification(modified, densities);
  }

  @Test public void testExistingSupersetCompatibleScreens() throws Exception {
    ImmutableList<String> densities = ImmutableList.of("ldpi");
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "<compatible-screens>",
        "  <screen android:screenSize='small' android:screenDensity='ldpi' />",
        "  <screen android:screenSize='normal' android:screenDensity='ldpi' />",
        "  <screen android:screenSize='large' android:screenDensity='ldpi' />",
        "  <screen android:screenSize='xlarge' android:screenDensity='ldpi' />",
        "  <screen android:screenSize='small' android:screenDensity='480' />",
        "  <screen android:screenSize='normal' android:screenDensity='480' />",
        "  <screen android:screenSize='large' android:screenDensity='480' />",
        "  <screen android:screenSize='xlarge' android:screenDensity='480' />",
        "</compatible-screens>",
        "</manifest>");
    Path modified = new DensitySpecificManifestProcessor(densities,
        tmp.resolve("manifest-filtered/AndroidManifest.xml")).process(manifest);
    assertThat((Object) modified).isNotNull();
    checkModification(modified, ImmutableList.<String>of("ldpi", "xxhdpi"));
  }

  @Test public void testMalformedManifest() throws Exception {
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "</manifest>");
    AndroidManifestProcessor.ManifestProcessingException e =
        assertThrows(
            AndroidManifestProcessor.ManifestProcessingException.class,
            () ->
                new DensitySpecificManifestProcessor(
                        ImmutableList.of("xhdpi"),
                        tmp.resolve("manifest-filtered/AndroidManifest.xml"))
                    .process(manifest));
    assertThat(e).hasMessageThat().contains("must be well-formed");
  }

  @Test public void testNoManifest() throws Exception {
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
    AndroidManifestProcessor.ManifestProcessingException e =
        assertThrows(
            AndroidManifestProcessor.ManifestProcessingException.class,
            () ->
                new DensitySpecificManifestProcessor(
                        ImmutableList.of("xhdpi"),
                        tmp.resolve("manifest-filtered/AndroidManifest.xml"))
                    .process(manifest));
    assertThat(e).hasMessageThat().contains("Premature end of file.");
  }

  @Test public void testNestedManifest() throws Exception {
    Path manifest = createManifest("<?xml version=\"1.0\" encoding=\"utf-8\"?>",
        "<manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "          package='com.google.test'>",
        "  <manifest xmlns:android='http://schemas.android.com/apk/res/android'",
        "            package='com.google.test'>",
        "  </manifest>",
        "</manifest>");
    AndroidManifestProcessor.ManifestProcessingException e =
        assertThrows(
            AndroidManifestProcessor.ManifestProcessingException.class,
            () ->
                new DensitySpecificManifestProcessor(
                        ImmutableList.of("xhdpi"),
                        tmp.resolve("manifest-filtered/AndroidManifest.xml"))
                    .process(manifest));
    assertThat(e).hasMessageThat().contains("does not contain exactly one <manifest>");
  }

  @Before
  public void setUpEnvironment() throws Exception {
    fs = Jimfs.newFileSystem();
    tmp = Files.createTempDirectory(this.getClass().getSimpleName());
  }

  @After
  public void cleanUpEnvironment() throws Exception {
    fs.close();
  }

  private Path createManifest(String... lines) throws IOException {
    final Path path = tmp.resolve("AndroidManifest.xml");
    Files.createDirectories(path.getParent());
    Files.deleteIfExists(path);
    try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
      writer.write(Joiner.on("\n").join(lines));
    }
    return path;
  }

  private void checkModification(Path manifest, List<String> densities) throws Exception {
    Set<String> sizeDensities = new HashSet<>();
    for (String density : densities) {
      for (String screenSize : SCREEN_SIZES) {
        sizeDensities.add(screenSize
            + PLAY_STORE_SUPPORTED_DENSITIES.get(density));
      }
    }

    DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    Document doc = db.parse(Files.newInputStream(manifest));
    NodeList compatibleScreensNodes = doc.getElementsByTagName("compatible-screens");
    assertThat(compatibleScreensNodes.getLength()).isEqualTo(1);
    Node compatibleScreens = compatibleScreensNodes.item(0);
    NodeList screens = doc.getElementsByTagName("screen");
    assertThat(screens.getLength()).isEqualTo(densities.size() * SCREEN_SIZES.size());
    for (int i = 0; i < screens.getLength(); i++) {
      Node s = screens.item(i);
      assertThat(s.getParentNode().isSameNode(compatibleScreens)).isTrue();
      if (s.getNodeType() == Node.ELEMENT_NODE) {
        Element screen = (Element) s;
        assertThat(
                sizeDensities.remove(
                    screen.getAttribute("android:screenSize")
                        + screen.getAttribute("android:screenDensity")))
            .isTrue();
      }
    }
    assertThat(sizeDensities).isEmpty();
  }

  private void checkCompatibleScreensOmitted(Path manifest) throws Exception {
    DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
    Document doc = db.parse(Files.newInputStream(manifest));
    NodeList compatibleScreensNodes = doc.getElementsByTagName("compatible-screens");
    assertThat(compatibleScreensNodes.getLength()).isEqualTo(0);
    NodeList screens = doc.getElementsByTagName("screen");
    assertThat(screens.getLength()).isEqualTo(0);
  }

}
