// 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 static com.google.devtools.build.android.ParsedAndroidDataBuilder.file;
import static com.google.devtools.build.android.ParsedAndroidDataBuilder.xml;

import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.jimfs.Jimfs;
import com.google.common.truth.Subject;
import com.google.devtools.build.android.AndroidDataBuilder.ResourceType;
import com.google.devtools.build.android.AndroidDataMerger.MergeConflictException;
import com.google.devtools.build.android.AndroidDataMerger.SourceChecker;
import com.google.devtools.build.android.xml.IdXmlResourceValue;
import com.google.devtools.build.android.xml.PublicXmlResourceValue;
import com.google.devtools.build.android.xml.SimpleXmlResourceValue;
import com.google.devtools.build.android.xml.SimpleXmlResourceValue.Type;
import com.google.devtools.build.android.xml.StyleableXmlResourceValue;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

/** Tests for {@link AndroidDataMerger}. */
@RunWith(JUnit4.class)
public class AndroidDataMergerTest {

  static final String XLIFF_NAMESPACE = "urn:oasis:names:tc:xliff:document:1.2";
  static final String XLIFF_PREFIX = "xliff";

  private FileSystem fileSystem;
  private FullyQualifiedName.Factory fqnFactory;
  private TestLoggingHandler loggingHandler;
  private Logger mergerLogger;

  @Before
  public void setUp() throws Exception {
    fileSystem = Jimfs.newFileSystem();
    fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.<String>of());
    mergerLogger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName());
    loggingHandler = new TestLoggingHandler();
    mergerLogger.addHandler(loggingHandler);
  }

  @After
  public void removeLoggingHandler() {
    mergerLogger.removeHandler(loggingHandler);
  }

  @Test
  public void mergeDirectDeps() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");

    DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/resources.xml"));
    DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml"));

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                file("layout/exit").source("res/layout/exit.xml"),
                xml("string/exit")
                    .source(directStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of()))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .source(primaryStrings.overwrite(directStrings))
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(file("layout/exit").root(directRoot).source("res/layout/exit.xml"))
                .combining(
                    xml("id/exit")
                        .root(directRoot)
                        .source("values/ids.xml")
                        .value(IdXmlResourceValue.of()))
                .build());

    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeDirectAndTransitiveDeps() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    DataSource directString = DataSource.of(directRoot.resolve("res/values/resources.xml"));
    DataSource primaryString =
        DataSource.of(primaryRoot.resolve("res").resolve("values/resources.xml"));

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(file("layout/enter").source("res/layout/enter.xml"))
            .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of()))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                file("layout/exit").source("res/layout/exit.xml"),
                xml("string/exit")
                    .source(directString)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of()))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .root(primaryRoot)
                        .source(primaryString.overwrite(directString))
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    file("layout/enter").root(transitiveRoot).source("res/layout/enter.xml"),
                    file("layout/exit").root(directRoot).source("res/layout/exit.xml"))
                .combining(
                    xml("id/exit")
                        .root(directRoot)
                        .source("values/ids.xml")
                        .value(IdXmlResourceValue.of()))
                .build());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeWithOverwriteInTransitiveDeps() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");
    Path descendentRoot = fileSystem.getPath("descendent");

    DataSource descendentLayout = DataSource.of(descendentRoot.resolve("res/layout/enter.xml"));
    DataSource transitiveLayout = DataSource.of(transitiveRoot.resolve("res/layout/enter.xml"));

    DataSource primaryString = DataSource.of(primaryRoot.resolve("res/values/resources.xml"));
    DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml"));

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(file("layout/enter").source(descendentLayout))
            .overwritable(file("layout/enter").source(transitiveLayout.overwrite(descendentLayout)))
            .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of()))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                file("layout/exit").source("res/layout/exit.xml"),
                xml("string/exit")
                    .source(directStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .combining(xml("id/exit").source("values/ids.xml").value(IdXmlResourceValue.of()))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/resources.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData transitive =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .source(primaryString.overwrite(directStrings))
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    file("layout/enter").source(transitiveLayout.overwrite(descendentLayout)),
                    file("layout/exit").root(directRoot).source("res/layout/exit.xml"))
                .combining(
                    xml("id/exit")
                        .root(directRoot)
                        .source("values/ids.xml")
                        .value(IdXmlResourceValue.of()))
                .build());
    assertAbout(unwrittenMergedAndroidData).that(transitive).isEqualTo(expected);
  }

  @Test
  public void mergeDirectConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")),
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);

    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    fqnFactory.parse("string/exit"),
                    DataResourceXml.createWithNoNamespace(
                        directRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")),
                    DataResourceXml.createWithNoNamespace(
                        directRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
                .toConflictMessage());
  }

  @Test
  public void mergeDirectConflictDuplicated() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")),
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger =
        AndroidDataMerger.createWithDefaultThreadPool(
            new SourceChecker() {
              @Override
              public boolean checkEquality(DataSource one, DataSource two) throws IOException {
                return one.equals(two);
              }
            });

    assertAbout(unwrittenMergedAndroidData)
        .that(merger.merge(transitiveDependency, directDependency, primary, false, true))
        .isEqualTo(
            UnwrittenMergedAndroidData.of(
                primary.getManifest(),
                ParsedAndroidDataBuilder.empty(),
                ParsedAndroidDataBuilder.buildOn(fqnFactory)
                    .overwritable(
                        xml("string/exit")
                            .root(directRoot)
                            .source("values/strings.xml")
                            .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                    .build()));
  }

  @Test
  public void mergeDirectConflictDuplicatedWithDifferentSources() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")),
                xml("string/exit")
                    .source("values/more_strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")),
                xml("string/another_key")
                    .source("values/more_strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "another way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    merger.merge(transitiveDependency, directDependency, primary, false, true);

    assertThat(loggingHandler.warnings).isEmpty();
  }

  @Test
  public void mergeDirectConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/strings.xml"));
    DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml"));

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .source(directStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")),
                xml("string/exit")
                    .source(directStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, true, true);
    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .source(primaryStrings.overwrite(directStrings))
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeTransitiveConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")),
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);

    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    fqnFactory.parse("string/exit"),
                    DataResourceXml.createWithNoNamespace(
                        transitiveRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")),
                    DataResourceXml.createWithNoNamespace(
                        transitiveRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
                .toConflictMessage());
  }

  @Test
  public void mergeTransitiveConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            // Two string/exit will create conflict.
            .overwritable(
                xml("string/exit")
                    .root(transitiveRoot.resolve("1"))
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")),
                xml("string/exit")
                    .root(transitiveRoot.resolve("2"))
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory).build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, true, true);
    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .root(primaryRoot)
                        .source("values/strings.xml")
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeDirectAndTransitiveConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    fqnFactory.parse("string/exit"),
                    DataResourceXml.createWithNoNamespace(
                        directRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")),
                    DataResourceXml.createWithNoNamespace(
                        transitiveRoot.resolve("res/values/strings.xml"),
                        SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
                .toConflictMessage());
  }

  @Test
  public void mergeDirectTransitivePrimaryConflictWithoutPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);

    FullyQualifiedName fullyQualifiedName = fqnFactory.parse("string/exit");
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                fullyQualifiedName,
                DataResourceXml.createWithNoNamespace(
                    directRoot.resolve("res/values/strings.xml"),
                    SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")),
                DataResourceXml.createWithNoNamespace(
                    transitiveRoot.resolve("res/values/strings.xml"),
                    SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
                .toConflictMessage());
  }

  @Test
  public void mergeDirectTransitivePrimaryConflictWithThrowOnConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source("values/strings.xml")
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    try {
      merger.merge(transitiveDependency, directDependency, primary, false, true);
      throw new Exception("Expected a MergeConflictException!");
    } catch (MergeConflictException e) {
      return;
    }
  }

  @Test
  public void mergeDirectTransitivePrimaryConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    DataSource primaryStrings = DataSource.of(primaryRoot.resolve("res/values/strings.xml"));
    DataSource directStrings = DataSource.of(directRoot.resolve("res/values/strings.xml"));
    DataSource transitiveStrings = DataSource.of(transitiveRoot.resolve("res/values/strings.xml"));

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source(transitiveStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "no way out")))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(
                xml("string/exit")
                    .source(directStrings)
                    .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "wrong way out")))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/strings.xml", ResourceType.VALUE, "<string name='exit'>way out</string>")
            .buildUnvalidated();

    UnwrittenMergedAndroidData data =
        AndroidDataMerger.createWithDefaults()
            .merge(transitiveDependency, directDependency, primary, true, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(fqnFactory)
                .overwritable(
                    xml("string/exit")
                        .source(primaryStrings.overwrite(directStrings))
                        .value(SimpleXmlResourceValue.createWithValue(Type.STRING, "way out")))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeDirectAndTransitiveNinepatchConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    // A drawable nine-patch png and plain png with the same base filename will conflict.
    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(file("drawable/rounded_corners").source("drawable/rounded_corners.9.png"))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(file("drawable/rounded_corners").source("drawable/rounded_corners.png"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    merger.merge(transitiveDependency, directDependency, primary, false, false);

    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    fqnFactory.parse("drawable/rounded_corners"),
                    DataValueFile.of(directRoot.resolve("res/drawable/rounded_corners.png")),
                    DataValueFile.of(transitiveRoot.resolve("res/drawable/rounded_corners.9.png")))
                .toConflictMessage());
  }

  @Test
  public void mergeAssetsDirectDeps() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset("bin/boojum")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.builder()
                .assets(file().root(primaryRoot).source("bin/boojum"))
                .build(),
            ParsedAndroidDataBuilder.builder()
                .assets(file().root(directRoot).source("hunting/of/the/snark.txt"))
                .build());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeCombiningResources() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .combining(
                xml("styleable/RubADubDub")
                    .source("values/attrs.xml")
                    .value(
                        StyleableXmlResourceValue.createAllAttrAsReferences(
                            fqnFactory.parse("attr/baker"))))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .combining(
                xml("styleable/RubADubDub")
                    .source("values/attrs.xml")
                    .value(
                        StyleableXmlResourceValue.createAllAttrAsReferences(
                            fqnFactory.parse("attr/candlestickmaker"))))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/attrs.xml",
                ResourceType.VALUE,
                "<declare-styleable name='RubADubDub'>",
                "  <attr name='butcher'/>",
                "</declare-styleable>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory)
                .combining(
                    xml("styleable/RubADubDub")
                        .source("values/attrs.xml")
                        .value(
                            StyleableXmlResourceValue.createAllAttrAsReferences(
                                fqnFactory.parse("attr/baker"),
                                fqnFactory.parse("attr/butcher"),
                                fqnFactory.parse("attr/candlestickmaker"))))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeCombiningResourcesWithNamespaces() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    // TODO(corysmith): Make conflict uris for a single prefix and error
    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .combining(
                xml("styleable/RubADubDub")
                    .source("values/attrs.xml")
                    .namespace(XLIFF_PREFIX, XLIFF_NAMESPACE)
                    .namespace("tools", "http://schemas.android.com/tools")
                    .value(
                        StyleableXmlResourceValue.createAllAttrAsReferences(
                            fqnFactory.parse("attr/baker"))))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .combining(
                xml("styleable/RubADubDub")
                    .source("values/attrs.xml")
                    .namespace(XLIFF_PREFIX, "wrong uri")
                    .value(
                        StyleableXmlResourceValue.createAllAttrAsReferences(
                            fqnFactory.parse("attr/candlestickmaker"))))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/attrs.xml",
                ResourceType.VALUE,
                "<declare-styleable name='RubADubDub'>",
                "  <attr name='butcher'/>",
                "</declare-styleable>")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory)
                .combining(
                    xml("styleable/RubADubDub")
                        .source("values/attrs.xml")
                        .namespace(XLIFF_PREFIX, "wrong uri")
                        .namespace("tools", "http://schemas.android.com/tools")
                        .value(
                            StyleableXmlResourceValue.createAllAttrAsReferences(
                                fqnFactory.parse("attr/baker"),
                                fqnFactory.parse("attr/butcher"),
                                fqnFactory.parse("attr/candlestickmaker"))))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeCombiningLayoutIDResources() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    DataSource transitiveLayout =
        DataSource.of(transitiveRoot.resolve("res/layout/transitive.xml"));
    DataSource directLayout = DataSource.of(directRoot.resolve("res/layout/direct.xml"));
    DataSource primaryLayout = DataSource.of(primaryRoot.resolve("res/layout/primary.xml"));

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .overwritable(file("layout/transitive").source(transitiveLayout))
            .combining(
                xml("id/back_door").source(transitiveLayout).value(IdXmlResourceValue.of()),
                xml("id/door").source("values/ids.xml").value(IdXmlResourceValue.of()),
                xml("id/slide").source(transitiveLayout).value(IdXmlResourceValue.of()))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .overwritable(file("layout/zzDirect").source(directLayout))
            .combining(
                xml("id/door").source(directLayout).value(IdXmlResourceValue.of()),
                xml("id/slide").source(directLayout).value(IdXmlResourceValue.of()),
                xml("id/window").source(directLayout).value(IdXmlResourceValue.of()))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "layout/primary.xml",
                ResourceType.LAYOUT,
                "<TextView android:id=\"@+id/door\"",
                "          android:text=\"this way ---> \"",
                "          android:layout_width=\"wrap_content\"",
                "          android:layout_height=\"wrap_content\" />",
                "<TextView android:id=\"@+id/window\"",
                "          android:text=\"no, not that way\"",
                "          android:layout_width=\"wrap_content\"",
                "          android:layout_height=\"wrap_content\" />")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory)
                .overwritable(file("layout/primary").source(primaryLayout))
                .combining(
                    xml("id/window").source("layout/primary.xml").value(IdXmlResourceValue.of()),
                    xml("id/door")
                        .root(transitiveRoot)
                        .source("values/ids.xml")
                        .value(IdXmlResourceValue.of()))
                .build(),
            ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
                .overwritable(
                    file("layout/transitive").source(transitiveLayout),
                    file("layout/zzDirect").source(directLayout))
                .combining(
                    xml("id/back_door").source(transitiveLayout).value(IdXmlResourceValue.of()),
                    xml("id/slide").source(directLayout).value(IdXmlResourceValue.of()))
                .build());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeCombiningPublicResources() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot, fqnFactory)
            .combining(
                xml("public/RubADubDub")
                    .source("values/public.xml")
                    .value(
                        PublicXmlResourceValue.create(
                            com.android.resources.ResourceType.STRING, Optional.of(0x7f040000))))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot, fqnFactory)
            .combining(
                xml("public/RubADubDub")
                    .source("values/public.xml")
                    .value(
                        PublicXmlResourceValue.create(
                            com.android.resources.ResourceType.DIMEN, Optional.of(0x7f020000))))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addResource(
                "values/public.xml",
                ResourceType.VALUE,
                "<public name='RubADubDub' type='color' id='0x7f030000' />")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot, fqnFactory)
                .combining(
                    xml("public/RubADubDub")
                        .source("values/public.xml")
                        .value(
                            PublicXmlResourceValue.of(
                                ImmutableMap.of(
                                    com.android.resources.ResourceType.COLOR,
                                    Optional.of(0x7f030000),
                                    com.android.resources.ResourceType.DIMEN,
                                    Optional.of(0x7f020000),
                                    com.android.resources.ResourceType.STRING,
                                    Optional.of(0x7f040000)))))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeAssetsDirectAndTransitiveDeps() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot)
            .assets(file().source("hunting/of/the/jubjub.bird"))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset("bin/boojum")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, false, true);

    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.builder()
                .assets(file().root(primaryRoot).source("bin/boojum"))
                .build(),
            ParsedAndroidDataBuilder.builder()
                .assets(
                    file().root(directRoot).source("hunting/of/the/snark.txt"),
                    file().root(transitiveRoot).source("hunting/of/the/jubjub.bird"))
                .build());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeAssetsDirectConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRootOne = fileSystem.getPath("directOne");
    Path directRootTwo = fileSystem.getPath("directTwo");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.builder()
            .assets(
                file().root(directRootOne).source("hunting/of/the/snark.txt"),
                file().root(directRootTwo).source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    RelativeAssetPath.Factory.of(directRootOne.resolve("assets"))
                        .create(directRootOne.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(directRootOne.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(directRootTwo.resolve("assets/hunting/of/the/snark.txt")))
                .toConflictMessage());
  }

  @Test
  public void mergeAssetsDirectConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRootOne = fileSystem.getPath("directOne");
    Path directRootTwo = fileSystem.getPath("directTwo");

    ParsedAndroidData transitiveDependency = ParsedAndroidDataBuilder.empty();
    String assetFile = "hunting/of/the/snark.txt";

    DataSource primarySource = DataSource.of(primaryRoot.resolve("assets/" + assetFile));
    DataSource directSourceOne = DataSource.of(directRootOne.resolve("assets/" + assetFile));
    DataSource directSourceTwo = DataSource.of(directRootTwo.resolve("assets/" + assetFile));

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.builder()
            .assets(
                file().root(directRootOne).source(directSourceOne),
                file().root(directRootTwo).source(directSourceTwo))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset(assetFile)
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, true, true);
    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.builder()
                .assets(
                    file()
                        .root(primaryRoot)
                        .source(primarySource.overwrite(directSourceOne, directSourceTwo)))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeAssetsTransitiveConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path transitiveRootOne = fileSystem.getPath("transitiveOne");
    Path transitiveRootTwo = fileSystem.getPath("transitiveTwo");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.builder()
            .assets(
                file().root(transitiveRootOne).source("hunting/of/the/snark.txt"),
                file().root(transitiveRootTwo).source("hunting/of/the/snark.txt"))
            .build();

    ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    RelativeAssetPath.Factory.of(transitiveRootOne.resolve("assets"))
                        .create(transitiveRootOne.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(transitiveRootOne.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(transitiveRootTwo.resolve("assets/hunting/of/the/snark.txt")))
                .toConflictMessage());
  }

  @Test
  public void mergeAssetsTransitiveConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset("hunting/of/the/snark.txt")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();
    UnwrittenMergedAndroidData data =
        merger.merge(transitiveDependency, directDependency, primary, true, true);
    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot)
                .assets(file().source("hunting/of/the/snark.txt"))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  @Test
  public void mergeAssetsDirectAndTransitiveConflict() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    RelativeAssetPath.Factory.of(directRoot.resolve("assets"))
                        .create(directRoot.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(directRoot.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(transitiveRoot.resolve("assets/hunting/of/the/snark.txt")))
                .toConflictMessage());
  }

  @Test
  public void mergeAssetsDirectTransitivePrimaryConflictWithoutPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset("hunting/of/the/snark.txt")
            .buildUnvalidated();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

    merger.merge(transitiveDependency, directDependency, primary, false, false);
    assertThat(loggingHandler.warnings)
        .containsExactly(
            MergeConflict.of(
                    RelativeAssetPath.Factory.of(directRoot.resolve("assets"))
                        .create(directRoot.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(directRoot.resolve("assets/hunting/of/the/snark.txt")),
                    DataValueFile.of(transitiveRoot.resolve("assets/hunting/of/the/snark.txt")))
                .toConflictMessage());
  }

  @Test
  public void mergeAssetsDirectTransitivePrimaryConflictWithPrimaryOverride() throws Exception {
    Path primaryRoot = fileSystem.getPath("primary");
    Path directRoot = fileSystem.getPath("direct");
    Path transitiveRoot = fileSystem.getPath("transitive");

    DataSource primarySource =
        DataSource.of(primaryRoot.resolve("assets/hunting/of/the/snark.txt"));
    DataSource directSource = DataSource.of(directRoot.resolve("assets/hunting/of/the/snark.txt"));

    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(transitiveRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    ParsedAndroidData directDependency =
        ParsedAndroidDataBuilder.buildOn(directRoot)
            .assets(file().source("hunting/of/the/snark.txt"))
            .build();

    UnvalidatedAndroidData primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .addAsset("hunting/of/the/snark.txt")
            .buildUnvalidated();

    UnwrittenMergedAndroidData data =
        AndroidDataMerger.createWithDefaults()
            .merge(transitiveDependency, directDependency, primary, true, true);
    
    UnwrittenMergedAndroidData expected =
        UnwrittenMergedAndroidData.of(
            primary.getManifest(),
            ParsedAndroidDataBuilder.buildOn(primaryRoot)
                .assets(file().source(primarySource.overwrite(directSource)))
                .build(),
            ParsedAndroidDataBuilder.empty());
    assertAbout(unwrittenMergedAndroidData).that(data).isEqualTo(expected);
  }

  final Subject.Factory<UnwrittenMergedAndroidDataSubject, UnwrittenMergedAndroidData>
      unwrittenMergedAndroidData = UnwrittenMergedAndroidDataSubject::new;

  private static final class TestLoggingHandler extends Handler {
    public final List<String> warnings = new ArrayList<String>();

    @Override
    public void publish(LogRecord record) {
      if (record.getLevel().equals(Level.WARNING)) {
        warnings.add(record.getMessage());
      }
    }

    @Override
    public void flush() {}

    @Override
    public void close() throws SecurityException {}
  }
}
