// Copyright 2019 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.xml;

import com.android.aapt.Resources.Attribute;
import com.android.aapt.Resources.CompoundValue;
import com.android.aapt.Resources.Value;
import com.google.auto.value.AutoValue;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.jimfs.Jimfs;
import com.google.common.truth.Subject;
import com.google.devtools.build.android.resources.Visibility;
import com.google.devtools.build.android.xml.AttrXmlResourceValue;
import com.google.devtools.build.android.xml.AttrXmlResourceValue.ReferenceResourceXmlAttrValue;
import com.google.devtools.build.android.xml.AttrXmlResourceValue.StringResourceXmlAttrValue;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
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.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;

/** Parameterized tests for {@link AndroidDataMerger} focusing on merging attribute resoutces. */
@RunWith(Parameterized.class)
public final class AttributeResourcesAndroidDataMergerTest {

  /** For test data readability, represent strength as a 2-state enum rather than a boolean. */
  enum Strength {
    STRONG,
    WEAK
  }

  @Parameters(name = "{index}: {0}")
  public static Collection<Object[]> data() {
    return Arrays.asList(
        new Object[][] {
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 1)
                .set2(Strength.STRONG, 0xFFFF)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry())),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries())))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.WEAK, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
                                        .value(
                                            AttrXmlResourceValue.weakFromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 0xFFFF)
                .set2(Strength.WEAK, 1)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(
                                            AttrXmlResourceValue.weakFromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 1)
                .set2(Strength.STRONG, 1)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(
                                            AttrXmlResourceValue.fromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.WEAK, 1)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(
                                            AttrXmlResourceValue.weakFromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 1)
                .set2(Strength.WEAK, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
                                        .value(
                                            AttrXmlResourceValue.fromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 0xFFFF)
                .set2(Strength.STRONG, 2)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(
                                            AttrXmlResourceValue.fromFormatEntries(
                                                StringResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 1)
                .set2(Strength.WEAK, 2)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry())),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    StringResourceXmlAttrValue.asEntry()))))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.STRONG, 2)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry())),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries(
                                    StringResourceXmlAttrValue.asEntry()))))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.WEAK, 2)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry())),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    StringResourceXmlAttrValue.asEntry()))))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 1)
                .set2(Strength.WEAK, 1)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
                                        .value(
                                            AttrXmlResourceValue.fromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.STRONG, 1)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(
                                            AttrXmlResourceValue.fromFormatEntries(
                                                ReferenceResourceXmlAttrValue.asEntry())))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 0xFFFF)
                .set2(Strength.STRONG, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(AttrXmlResourceValue.fromFormatEntries()))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 0xFFFF)
                .set2(Strength.WEAK, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(AttrXmlResourceValue.weakFromFormatEntries()))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 0xFFFF)
                .set2(Strength.WEAK, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr1.overwrite(ctx.transitiveAttr2))
                                        .value(AttrXmlResourceValue.fromFormatEntries()))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 0xFFFF)
                .set2(Strength.STRONG, 0xFFFF)
                .setExpectedMergedAndroidData(
                    ctx ->
                        UnwrittenMergedAndroidData.of(
                            ctx.primary.getManifest(),
                            ParsedAndroidDataBuilder.empty(),
                            ParsedAndroidDataBuilder.buildOn(ctx.fqnFactory)
                                .overwritable(
                                    xml("attr/ambiguousName")
                                        .root(ctx.primaryRoot)
                                        .source(ctx.transitiveAttr2.overwrite(ctx.transitiveAttr1))
                                        .value(AttrXmlResourceValue.fromFormatEntries()))
                                .build()))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.STRONG, 0xFFFF)
                .set2(Strength.WEAK, 1)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries()),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry()))))
                .build()
          },
          {
            TestParameters.builder()
                .set1(Strength.WEAK, 1)
                .set2(Strength.STRONG, 0xFFFF)
                .setExpectedMergeConflict(
                    ctx ->
                        MergeConflict.of(
                            ctx.fqnFactory.parse("attr/ambiguousName"),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot1.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.weakFromFormatEntries(
                                    ReferenceResourceXmlAttrValue.asEntry())),
                            DataResourceXml.createWithNoNamespace(
                                ctx.transitiveRoot2.resolve("res/values/attrs.xml"),
                                AttrXmlResourceValue.fromFormatEntries())))
                .build()
          }
        });
  }

  @Parameter public TestParameters testParameters;

  private FullyQualifiedName.Factory fqnFactory;
  private TestLoggingHandler loggingHandler;
  private Logger mergerLogger;
  private Path primaryRoot;
  private Path transitiveRoot1;
  private Path transitiveRoot2;
  private DataSource transitiveAttr1;
  private DataSource transitiveAttr2;
  private UnvalidatedAndroidData primary;

  @Before
  public void setUp() throws Exception {
    FileSystem fileSystem = Jimfs.newFileSystem();
    fqnFactory = FullyQualifiedName.Factory.from(ImmutableList.of());
    mergerLogger = Logger.getLogger(AndroidDataMerger.class.getCanonicalName());
    loggingHandler = new TestLoggingHandler();
    mergerLogger.addHandler(loggingHandler);
    primaryRoot = fileSystem.getPath("primary");
    transitiveRoot1 = fileSystem.getPath("transitive1");
    transitiveRoot2 = fileSystem.getPath("transitive2");
    transitiveAttr1 =
        DataSource.of(
            DependencyInfo.UNKNOWN, transitiveRoot1.resolve("res").resolve("values/attrs.xml"));
    transitiveAttr2 =
        DataSource.of(
            DependencyInfo.UNKNOWN, transitiveRoot2.resolve("res").resolve("values/attrs.xml"));
    primary =
        AndroidDataBuilder.of(primaryRoot)
            .createManifest("AndroidManifest.xml", "com.google.mergetest")
            .buildUnvalidated();
  }

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

  @Test
  public void test() throws Exception {
    ParsedAndroidData transitiveDependency =
        ParsedAndroidDataBuilder.buildOn(fqnFactory)
            .overwritable(
                xml("attr/ambiguousName")
                    .root(transitiveRoot1)
                    .source("values/attrs.xml")
                    .value(
                        AttrXmlResourceValue.from(
                            Value.newBuilder()
                                .setCompoundValue(
                                    CompoundValue.newBuilder()
                                        .setAttr(
                                            Attribute.newBuilder()
                                                .setFormatFlags(testParameters.formatFlags1())))
                                .setWeak(testParameters.strength1() == Strength.WEAK)
                                .build(),
                            Visibility.UNKNOWN)))
            .overwritable(
                xml("attr/ambiguousName")
                    .root(transitiveRoot2)
                    .source("values/attrs.xml")
                    .value(
                        AttrXmlResourceValue.from(
                            Value.newBuilder()
                                .setCompoundValue(
                                    CompoundValue.newBuilder()
                                        .setAttr(
                                            Attribute.newBuilder()
                                                .setFormatFlags(testParameters.formatFlags2())))
                                .setWeak(testParameters.strength2() == Strength.WEAK)
                                .build(),
                            Visibility.UNKNOWN)))
            .build();

    ParsedAndroidData directDependency = ParsedAndroidDataBuilder.empty();

    AndroidDataMerger merger = AndroidDataMerger.createWithDefaults();

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

    if (testParameters.expectedMergedAndroidData().isPresent()) {
      assertAbout(unwrittenMergedAndroidData)
          .that(data)
          .isEqualTo(testParameters.expectedMergedAndroidData().get().apply(this));
    }

    if (testParameters.expectedMergeConflict().isPresent()) {
      assertThat(loggingHandler.warnings)
          .containsExactly(
              testParameters.expectedMergeConflict().get().apply(this).toConflictMessage());
    } else {
      assertThat(loggingHandler.warnings).isEmpty();
    }
  }

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

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

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

    @Override
    public void flush() {}

    @Override
    public void close() {}
  }

  @AutoValue
  abstract static class TestParameters {
    abstract Strength strength1();

    abstract int formatFlags1();

    abstract Strength strength2();

    abstract int formatFlags2();

    abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData>>
        expectedMergedAndroidData();

    abstract Optional<Function<AttributeResourcesAndroidDataMergerTest, MergeConflict>>
        expectedMergeConflict();

    @Override
    public final String toString() {
      return Joiner.on(", ")
          .join(
              strength1(),
              formatFlags1(),
              strength2(),
              formatFlags2(),
              expectedMergeConflict().isPresent()
                  ? "conflict expected"
                  : "successful merge expected");
    }

    static Builder builder() {
      return new AutoValue_AttributeResourcesAndroidDataMergerTest_TestParameters.Builder();
    }

    @AutoValue.Builder
    abstract static class Builder {

      Builder set1(Strength strength, int formatFlags) {
        return setStrength1(strength).setFormatFlags1(formatFlags);
      }

      Builder set2(Strength strength, int formatFlags) {
        return setStrength2(strength).setFormatFlags2(formatFlags);
      }

      abstract Builder setStrength1(Strength value);

      abstract Builder setFormatFlags1(int value);

      abstract Builder setStrength2(Strength value);

      abstract Builder setFormatFlags2(int value);

      abstract Builder setExpectedMergedAndroidData(
          Function<AttributeResourcesAndroidDataMergerTest, UnwrittenMergedAndroidData> value);

      abstract Builder setExpectedMergeConflict(
          Function<AttributeResourcesAndroidDataMergerTest, MergeConflict> value);

      abstract TestParameters build();
    }
  }
}
