/*
 * Copyright 2008 Google Inc.
 *
 * 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.gwt.dev.resource.impl;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.resource.Resource;
import com.google.gwt.thirdparty.guava.common.io.CharStreams;
import com.google.gwt.thirdparty.guava.common.io.Closeables;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Tests {@link ResourceOracleImpl}.
 *
 * <pre>
 * Important states to test:
 * - No class path entries
 * - A single class path entries
 *   - init/add/update/remove
 * - Multiple class path entries
 *   - init/add/update/remove
 *   - same as previous with shadowing
 *   - same as previous with superceding
 * </pre>
 */
public class ResourceOracleImplTest extends AbstractResourceOrientedTestBase {

  // Starts empty but can change during tests.
  private static class MOCK_CPE0 extends MockClassPathEntry {
    public MOCK_CPE0() {
      super("/cpe0/");
    }
  }

  private static class MOCK_CPE3 extends MockClassPathEntry {
    public MOCK_CPE3() {
      super("/cpe3/");
    }
  }

  private static class ResourceOracleSnapshot {
    private final Set<String> pathNames;
    private final Set<Resource> resources;

    public ResourceOracleSnapshot(ResourceOracleImpl oracle) {
      resources = oracle.getResources();
      pathNames = oracle.getPathNames();
    }

    public void assertCollectionsConsistent(int expectedSize) {
      assertEquals(expectedSize, resources.size());
      assertEquals(resources.size(), pathNames.size());

      // Ensure that every resource path is in the set.
      for (Resource r : resources) {
        assertTrue(pathNames.contains(r.getPath()));
      }
    }

    public void assertNotSameCollections(ResourceOracleSnapshot other) {
      assertNotSame(resources, other.resources);
      assertNotSame(pathNames, other.pathNames);
    }

    public void assertPathIncluded(String path) {
      assertNotNull(findResourceWithPath(path));
    }

    /**
     * Asserts that a resource having the specified path is present and that it was contributed by
     * the specified classpath entry.
     */
    public void assertPathIncluded(String expectedPath, ClassPathEntry expectedCpe) {
      AbstractResource resource = findResourceWithPath(expectedPath);
      assertNotNull(resource);
      String cpeLocation = expectedCpe.getLocation();
      if (cpeLocation.endsWith(".jar") || cpeLocation.endsWith(".zip")) {
        assertTrue(resource.getLocation().startsWith("jar:" + cpeLocation));
      } else {
        assertTrue(resource.getLocation().startsWith(cpeLocation));
      }
    }

    public void assertPathNotIncluded(String path) {
      assertNull(findResourceWithPath(path));
    }

    public void assertSameCollections(ResourceOracleSnapshot other) {
      assertResourcesEqual(resources, other.resources);
      assertEquals(pathNames, other.pathNames);
    }

    public AbstractResource findResourceWithPath(String path) {
      for (Resource r : resources) {
        if (r.getPath().equals(path)) {
          return (AbstractResource) r;
        }
      }
      return null;
    }

    @SuppressWarnings("deprecation")
    public void assertResourcesGetURL() throws IOException {
      for (Resource resource : resources) {
        URL url = resource.getURL();
        assertNotNull("Resource " + resource + " had a null getURL()", url);

        InputStream is = resource.openContents();
        assertNotNull(is);

        String urlString;
        try (InputStream in = url.openStream()) {
          urlString = CharStreams.toString(new InputStreamReader(in, StandardCharsets.UTF_8));
        }
        String inputString = new String(is.readAllBytes(), StandardCharsets.UTF_8);
        assertEquals(urlString, inputString);
      }
    }
  }

  public static void assertResourcesEqual(Collection<Resource> expected,
      Collection<Resource> actual) {
    Set<String> expectedLocs = new HashSet<String>();
    for (Resource resource : expected) {
      expectedLocs.add(resource.getLocation());
    }
    Set<String> actualLocs = new HashSet<String>();
    for (Resource resource : actual) {
      actualLocs.add(resource.getLocation());
    }
    assertEquals(expectedLocs, actualLocs);
  }

  public static void assertResourcesEqual(Map<String, Resource> expected,
      Map<String, Resource> actual) {
    assertResourcesEqual(expected.values(), actual.values());
  }

  private static PathPrefix makeJavaLangPrefix() {
    return new PathPrefix("java/lang/", null, false);
  }

  private static PathPrefix makeBarPrefix() {
    return new PathPrefix("org/example/bar/client/", null, false);
  }

  private static PathPrefix makeRerootBarPrefix() {
    return new PathPrefix("org/example/bar/client/", null, true);
  }

  private static PathPrefix makeRerootFooPrefix() {
    return new PathPrefix("org/example/foo/client/", null, true);
  }

  private static PathPrefix makeTranslatablePrefix() {
    return new PathPrefix("translatable/", null, true);
  }

  public void testCachingOfJarResources() throws IOException,
      URISyntaxException {
    TreeLogger logger = createTestTreeLogger();
    File jarFile = findFile("com/google/gwt/dev/resource/impl/testdata/cpe1.jar");
    ClassPathEntry cpe1jar = ZipFileClassPathEntry.get(jarFile);

    // test basic caching
    PathPrefixSet pps1 = new PathPrefixSet();
    pps1.add(new PathPrefix("com/google/gwt/", null, false));
    Map<AbstractResource, ResourceResolution> resourceMap1 =
        cpe1jar.findApplicableResources(logger, pps1);
    assertSame(resourceMap1, cpe1jar.findApplicableResources(logger, pps1));

    // test that cache is invalidated if PathPrefixSet is modified.
    pps1.add(new PathPrefix("com/google/gwt/user/", null, false));
    Map<AbstractResource, ResourceResolution> resourceMap2 =
        cpe1jar.findApplicableResources(logger, pps1);
    assertNotSame(resourceMap1, resourceMap2);

    PathPrefixSet pps2 = new PathPrefixSet();
    pps2.add(new PathPrefix("org/example/bar/", null, false));
    Map<AbstractResource, ResourceResolution> resourceMap3 =
        cpe1jar.findApplicableResources(logger, pps2);
    // check that the entry did go in the cache
    assertSame(resourceMap3, cpe1jar.findApplicableResources(logger, pps2));

    // check that the cache does not thrash
    assertSame(resourceMap2, cpe1jar.findApplicableResources(logger, pps1));
    assertSame(resourceMap3, cpe1jar.findApplicableResources(logger, pps2));
  }

  public void testGetUrlOnResources() throws URISyntaxException, IOException {
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe1dir = getClassPathEntry1AsDirectory();
    ClassPathEntry cpe1zip = getClassPathEntry1AsZip();

    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
    ClassPathEntry cpe2dir = getClassPathEntry2AsDirectory();
    ClassPathEntry cpe2zip = getClassPathEntry2AsZip();

    testGetURLOnResourcesInCPE(cpe1jar);
    testGetURLOnResourcesInCPE(cpe1dir);
    testGetURLOnResourcesInCPE(cpe1zip);
    testGetURLOnResourcesInCPE(cpe2jar);
    testGetURLOnResourcesInCPE(cpe2dir);
    testGetURLOnResourcesInCPE(cpe2zip);
  }

  private void testGetURLOnResourcesInCPE(ClassPathEntry cpe) throws IOException {
    TreeLogger logger = createTestTreeLogger();

    ResourceOracleImpl oracle = createResourceOracle(cpe);
    ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
    s.assertResourcesGetURL();
  }

  /**
   * Test that ResourceOracleImpl preserves the order in which the same logical
   * resource is occurs in multiple ClassPathEntries.
   *
   * @throws URISyntaxException
   * @throws IOException
   */
  public void testClassPathOrderIsHonored() throws IOException,
      URISyntaxException {
    TreeLogger logger = createTestTreeLogger();
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();

    ClassPathEntry[] cp12 = new ClassPathEntry[]{cpe1jar, cpe2jar};
    ClassPathEntry[] cp21 = new ClassPathEntry[]{cpe2jar, cpe1jar};
    String resKeyNormal = "org/example/bar/client/BarClient2.txt";
    String resKeyReroot = "BarClient2.txt";
    testResourceInCPE(logger, resKeyNormal, cpe1jar, cp12, makeBarPrefix());
    testResourceInCPE(logger, resKeyReroot, cpe1jar, cp12,
        makeRerootBarPrefix());
    testResourceInCPE(logger, resKeyNormal, cpe2jar, cp21, makeBarPrefix());
    testResourceInCPE(logger, resKeyReroot, cpe2jar, cp21,
        makeRerootBarPrefix());
  }

  public void testNoClassPathEntries() {
    TreeLogger logger = createTestTreeLogger();
    ResourceOracleImpl oracle = createResourceOracle(new MOCK_CPE0());
    ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
    s.assertCollectionsConsistent(0);
  }

  /**
   * Test that ResourceOracleImpl prefers the order of path prefixes over
   * ClassPathEntries.
   * <p>
   * cpe1 contains org/example/bar/client/BarClient1.txt and cpe2 contains
   * org/example/foo/client/BarClient1.txt
   *
   * @throws URISyntaxException
   * @throws IOException
   */
  public void testPathPrefixOrderPreferredOverClasspath() throws IOException,
      URISyntaxException {
    TreeLogger logger = createTestTreeLogger();
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();

    ClassPathEntry[] cp12 = new ClassPathEntry[]{cpe1jar, cpe2jar};
    ClassPathEntry[] cp21 = new ClassPathEntry[]{cpe2jar, cpe1jar};

    String keyReroot = "BarClient1.txt";

    // Resource in cpe2 wins because pp2 comes later.
    testResourceInCPE(logger, keyReroot, cpe2jar, cp12, makeRerootBarPrefix(),
        makeRerootFooPrefix());
    // Order of specifying classpath is reversed, it still matches cpe2.
    testResourceInCPE(logger, keyReroot, cpe2jar, cp21, makeRerootBarPrefix(),
        makeRerootFooPrefix());

    // Resource in cpe1 wins because pp1 comes later.
    testResourceInCPE(logger, keyReroot, cpe1jar, cp12, makeRerootFooPrefix(),
        makeRerootBarPrefix());
    // Order of specifying classpath is reversed, it still matches cpe1.
    testResourceInCPE(logger, keyReroot, cpe1jar, cp21, makeRerootFooPrefix(),
        makeRerootBarPrefix());
  }

  /**
   * Tests the actual reading of resources.
   *
   * @throws URISyntaxException
   * @throws IOException
   */
  public void testReadingResource() throws IOException, URISyntaxException {
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe1dir = getClassPathEntry1AsDirectory();
    ClassPathEntry cpe1zip = getClassPathEntry1AsZip();

    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
    ClassPathEntry cpe2dir = getClassPathEntry2AsDirectory();
    ClassPathEntry cpe2zip = getClassPathEntry2AsZip();

    testReadingResource(cpe1jar, cpe2jar);
    testReadingResource(cpe1dir, cpe2jar);
    testReadingResource(cpe1zip, cpe2jar);

    testReadingResource(cpe1jar, cpe2dir);
    testReadingResource(cpe1dir, cpe2dir);
    testReadingResource(cpe1zip, cpe2dir);

    testReadingResource(cpe1jar, cpe2zip);
    testReadingResource(cpe1dir, cpe2zip);
    testReadingResource(cpe1zip, cpe2zip);
  }

  /**
   * Verify ResourceOracleImpls created from the same state return the same list of
   * ClassPathEntries.
   */
  public void testSameClassPathEntries() {
    TreeLogger logger = createTestTreeLogger();
    ResourceOracleImpl oracle = new ResourceOracleImpl(logger);
    List<ClassPathEntry> classPathEntries = oracle.getClassPathEntries();

    oracle = new ResourceOracleImpl(logger);
    List<ClassPathEntry> classPathEntries2 = oracle.getClassPathEntries();
    assertEquals(classPathEntries.size(), classPathEntries2.size());
    for (int i = 0; i < classPathEntries2.size(); ++i) {
      assertEquals(classPathEntries.get(i), classPathEntries2.get(i));
    }
  }

  public void testResourceAddition() throws IOException, URISyntaxException {
    ClassPathEntry cpe1mock = getClassPathEntry1AsMock();
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe1dir = getClassPathEntry1AsDirectory();
    ClassPathEntry cpe1zip = getClassPathEntry1AsZip();

    ClassPathEntry cpe2mock = getClassPathEntry2AsMock();
    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
    ClassPathEntry cpe2dir = getClassPathEntry2AsDirectory();
    ClassPathEntry cpe2zip = getClassPathEntry2AsZip();

    testResourceAddition(cpe1jar, cpe2jar);
    testResourceAddition(cpe1dir, cpe2jar);
    testResourceAddition(cpe1mock, cpe2jar);
    testResourceAddition(cpe1zip, cpe2jar);

    testResourceAddition(cpe1jar, cpe2dir);
    testResourceAddition(cpe1dir, cpe2dir);
    testResourceAddition(cpe1mock, cpe2dir);
    testResourceAddition(cpe1zip, cpe2dir);

    testResourceAddition(cpe1jar, cpe2mock);
    testResourceAddition(cpe1dir, cpe2mock);
    testResourceAddition(cpe1mock, cpe2mock);
    testResourceAddition(cpe1zip, cpe2mock);

    testResourceAddition(cpe1jar, cpe2zip);
    testResourceAddition(cpe1dir, cpe2zip);
    testResourceAddition(cpe1mock, cpe2zip);
    testResourceAddition(cpe1zip, cpe2zip);
  }

  public void testResourceDeletion() throws IOException, URISyntaxException {
    ClassPathEntry cpe1mock = getClassPathEntry1AsMock();
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe1dir = getClassPathEntry1AsDirectory();
    ClassPathEntry cpe1zip = getClassPathEntry1AsZip();

    ClassPathEntry cpe2mock = getClassPathEntry2AsMock();
    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
    ClassPathEntry cpe2dir = getClassPathEntry2AsDirectory();
    ClassPathEntry cpe2zip = getClassPathEntry2AsZip();

    testResourceDeletion(cpe1jar, cpe2jar);
    testResourceDeletion(cpe1dir, cpe2jar);
    testResourceDeletion(cpe1mock, cpe2jar);
    testResourceDeletion(cpe1zip, cpe2jar);

    testResourceDeletion(cpe1jar, cpe2dir);
    testResourceDeletion(cpe1dir, cpe2dir);
    testResourceDeletion(cpe1mock, cpe2dir);
    testResourceDeletion(cpe1zip, cpe2dir);

    testResourceDeletion(cpe1jar, cpe2mock);
    testResourceDeletion(cpe1dir, cpe2mock);
    testResourceDeletion(cpe1mock, cpe2mock);
    testResourceDeletion(cpe1zip, cpe2mock);

    testResourceDeletion(cpe1jar, cpe2zip);
    testResourceDeletion(cpe1dir, cpe2zip);
    testResourceDeletion(cpe1mock, cpe2zip);
    testResourceDeletion(cpe1zip, cpe2zip);
  }

  public void testResourceModification() throws IOException, URISyntaxException {
    ClassPathEntry cpe1mock = getClassPathEntry1AsMock();
    ClassPathEntry cpe1jar = getClassPathEntry1AsJar();
    ClassPathEntry cpe1dir = getClassPathEntry1AsDirectory();
    ClassPathEntry cpe1zip = getClassPathEntry1AsZip();

    ClassPathEntry cpe2mock = getClassPathEntry2AsMock();
    ClassPathEntry cpe2jar = getClassPathEntry2AsJar();
    ClassPathEntry cpe2dir = getClassPathEntry2AsDirectory();
    ClassPathEntry cpe2zip = getClassPathEntry2AsZip();

    testResourceModification(cpe1jar, cpe2jar);
    testResourceModification(cpe1dir, cpe2jar);
    testResourceModification(cpe1mock, cpe2jar);
    testResourceModification(cpe1zip, cpe2jar);

    testResourceModification(cpe1jar, cpe2dir);
    testResourceModification(cpe1dir, cpe2dir);
    testResourceModification(cpe1mock, cpe2dir);
    testResourceModification(cpe1zip, cpe2dir);

    testResourceModification(cpe1jar, cpe2mock);
    testResourceModification(cpe1dir, cpe2mock);
    testResourceModification(cpe1mock, cpe2mock);
    testResourceModification(cpe1zip, cpe2mock);

    testResourceModification(cpe1jar, cpe2zip);
    testResourceModification(cpe1dir, cpe2zip);
    testResourceModification(cpe1mock, cpe2zip);
    testResourceModification(cpe1zip, cpe2zip);

    /*
     * TODO(bruce): figure out a good way to test real resource modifications of
     * jar files and directories
     */
  }

  /**
   * Ensure refresh is stable when multiple classpaths + multiple path prefixes
   * all include the same resource.
   */
  public void testStableRefreshOnBlot() {
    TreeLogger logger = createTestTreeLogger();
    ClassPathEntry cpe1 = getClassPathEntry1AsMock();
    ClassPathEntry cpe2 = getClassPathEntry2AsMock();
    testResourceInCPE(logger, "org/example/bar/client/BarClient2.txt", cpe1,
        new ClassPathEntry[]{cpe1, cpe2}, makeBarPrefix(), new PathPrefix(
            "org/example/bar/", null, false));
  }

  /**
   * Ensure refresh is stable when multiple classpaths + multiple path prefixes
   * all include the same resource.
   */
  public void testSuperSourceSupercedesSource() {
    TreeLogger logger = createTestTreeLogger();

    MockClassPathEntry cpe1 = new MockClassPathEntry("/cpe1/");
    cpe1.addResource("java/lang/Object.java");

    MockClassPathEntry cpe2 = new MockClassPathEntry("/cpe2/");
    cpe2.addResource("translatable/java/lang/Object.java");

    // Ensure the translatable overrides the basic despite swapping CPE order.
    testResourceInCPE(logger, "java/lang/Object.java", cpe2,
        new ClassPathEntry[]{cpe1, cpe2}, makeJavaLangPrefix(),
        makeTranslatablePrefix());
    testResourceInCPE(logger, "java/lang/Object.java", cpe2,
        new ClassPathEntry[]{cpe2, cpe1}, makeJavaLangPrefix(),
        makeTranslatablePrefix());

    // Ensure the translatable overrides the basic despite swapping PPS order.
    testResourceInCPE(logger, "java/lang/Object.java", cpe2,
        new ClassPathEntry[]{cpe1, cpe2}, makeTranslatablePrefix(),
        makeJavaLangPrefix());
    testResourceInCPE(logger, "java/lang/Object.java", cpe2,
        new ClassPathEntry[]{cpe2, cpe1}, makeTranslatablePrefix(),
        makeJavaLangPrefix());
  }

  private void assertJarEntry(ClassPathEntry classPathEntry, String expected) {
    assertTrue("Should be instance of ZipFileClassPathEntry",
        classPathEntry instanceof ZipFileClassPathEntry);
    ZipFileClassPathEntry zipCPE = (ZipFileClassPathEntry) classPathEntry;
    String jar = zipCPE.getLocation();
    assertTrue("URL should contain " + expected, jar.contains(expected));
  }

  /**
   * Creates an array of class path entries, setting up each one with a
   * well-known set of client prefixes.
   */
  private ResourceOracleImpl createResourceOracle(ClassPathEntry... entries) {
    PathPrefixSet pps = new PathPrefixSet();
    pps.add(new PathPrefix("com/google/gwt/user/client/", null));
    pps.add(new PathPrefix("org/example/bar/client/", null));
    pps.add(new PathPrefix("org/example/foo/client/", null));
    pps.add(new PathPrefix("com/google/gwt/i18n/client/", null));

    List<ClassPathEntry> classPath = new ArrayList<ClassPathEntry>();
    for (ClassPathEntry entry : entries) {
      classPath.add(entry);
    }
    ResourceOracleImpl oracle = new ResourceOracleImpl(classPath);
    oracle.setPathPrefixes(pps);
    return oracle;
  }

  private ResourceOracleSnapshot refreshAndSnapshot(TreeLogger logger,
      ResourceOracleImpl oracle) {
    oracle.scanResources(logger);
    ResourceOracleSnapshot snapshot1 = new ResourceOracleSnapshot(oracle);
    oracle.scanResources(logger);
    ResourceOracleSnapshot snapshot2 = new ResourceOracleSnapshot(oracle);
    snapshot1.assertSameCollections(snapshot2);
    return snapshot1;
  }

  private void testReadingResource(ClassPathEntry cpe1, ClassPathEntry cpe2)
      throws IOException {
    TreeLogger logger = createTestTreeLogger();

    String commandPath = "com/google/gwt/user/client/Command.java";
    String messagesPath = "com/google/gwt/i18n/client/Messages.java";

    String userPackage = "cpe1.com.google.gwt.user.client";
    String i18nPackage = "cpe2.com.google.gwt.i18n.client";

    ResourceOracleImpl oracle = createResourceOracle(cpe1, cpe2);
    ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
    s.assertCollectionsConsistent(10);

    // Read a resource in cpe1.
    readAndAssertResource(oracle, commandPath, userPackage);

    // Read a resource in cpe2.
    readAndAssertResource(oracle, messagesPath, i18nPackage);

    // Read relative path.
    readAndAssertResource(oracle, "com/google/gwt/i18n/../i18n/client/Messages.java", i18nPackage);

    // Read path that doesn't exist.
    assertNull(oracle.getResource("com/google/gwt/i18n/client/DoesntExist.java"));
  }

  private void readAndAssertResource(ResourceOracleImpl oracle, String path, String expectedPackage)
      throws IOException {
    Resource res = oracle.getResource(path);
    assertNotNull(res);
    BufferedReader rdr = null;
    try {
      InputStream is = res.openContents();
      assertNotNull(is);
      rdr = new BufferedReader(new InputStreamReader(is));
      // Skip lines until package line is found.
      String line = rdr.readLine();
      while (line != null && !line.startsWith("package")) {
        line = rdr.readLine();
      }
      assertTrue(line != null
          && line.indexOf("package com.google.gwt.dev.resource.impl.testdata." + expectedPackage
              + ";") >= 0);
    } finally {
      Closeables.closeQuietly(rdr);
    }
  }

  private void testResourceAddition(ClassPathEntry cpe1, ClassPathEntry cpe2) {
    TreeLogger logger = createTestTreeLogger();

    MOCK_CPE0 cpe0 = new MOCK_CPE0();
    MOCK_CPE3 cpe3 = new MOCK_CPE3();
    ResourceOracleImpl oracle = createResourceOracle(cpe0, cpe1, cpe2, cpe3);

    {
      /*
       * Ensure it's correct as a baseline. These tests have hard-coded
       * assumptions about the contents of each classpath entry.
       */
      ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
      s.assertCollectionsConsistent(10);
      s.assertPathIncluded("com/google/gwt/user/client/Command.java");
      s.assertPathIncluded("com/google/gwt/user/client/Timer.java");
      s.assertPathIncluded("com/google/gwt/user/client/ui/Widget.java");
      s.assertPathIncluded("org/example/bar/client/BarClient1.txt");
      s.assertPathIncluded("org/example/bar/client/BarClient2.txt");
      s.assertPathIncluded("org/example/bar/client/BarClient3.txt");
      s.assertPathIncluded("org/example/bar/client/etc/BarEtc.txt");
      s.assertPathIncluded("org/example/foo/client/FooClient.java");
      s.assertPathIncluded("com/google/gwt/i18n/client/Messages.java");
    }

    {
      /*
       * Add duplicate resources later in the classpath, which won't be found.
       * Consequently, the collections' identities should not change.
       */
      cpe3.addResource("com/google/gwt/user/client/Command.java");
      cpe3.addResource("com/google/gwt/user/client/Timer.java");
      cpe3.addResource("com/google/gwt/bar/client/etc/BarEtc.txt");

      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertCollectionsConsistent(10);
      after.assertSameCollections(before);
    }

    {
      /*
       * Add a unique resource later in the classpath, which will be found.
       * Consequently, the collections' identities should change.
       */
      cpe3.addResource("com/google/gwt/i18n/client/Constants.java");

      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertCollectionsConsistent(11);
      after.assertNotSameCollections(before);
      after.assertPathIncluded("com/google/gwt/i18n/client/Constants.java");
    }
  }

  private void testResourceDeletion(ClassPathEntry cpe1, ClassPathEntry cpe2) {
    TreeLogger logger = createTestTreeLogger();

    MOCK_CPE0 cpe0 = new MOCK_CPE0();
    MOCK_CPE3 cpe3 = new MOCK_CPE3();
    ResourceOracleImpl oracle = createResourceOracle(cpe0, cpe1, cpe2, cpe3);

    /*
     * Intentionally add some duplicate resources.
     */
    cpe0.addResource("com/google/gwt/user/client/Command.java");
    cpe0.addResource("com/google/gwt/i18n/client/Constants.java");
    cpe3.addResource("com/google/gwt/user/client/Command.java");
    cpe3.addResource("com/google/gwt/i18n/client/Constants.java");

    {
      /*
       * Ensure it's correct as a baseline. These tests have hard-coded
       * assumptions about the contents of each classpath entry.
       */
      ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
      s.assertCollectionsConsistent(11);
      s.assertPathIncluded("com/google/gwt/user/client/Command.java");
      s.assertPathIncluded("com/google/gwt/user/client/Timer.java");
      s.assertPathIncluded("com/google/gwt/user/client/ui/Widget.java");
      s.assertPathIncluded("org/example/bar/client/BarClient1.txt");
      s.assertPathIncluded("org/example/bar/client/BarClient2.txt");
      s.assertPathIncluded("org/example/bar/client/BarClient3.txt");
      s.assertPathIncluded("org/example/bar/client/etc/BarEtc.txt");
      s.assertPathIncluded("org/example/foo/client/FooClient.java");
      s.assertPathIncluded("com/google/gwt/i18n/client/Messages.java");
      s.assertPathIncluded("com/google/gwt/i18n/client/Constants.java");
    }

    {
      /*
       * Remove a shadowed resource, which shouldn't have been found anyway.
       * Consequently, the collections' identities should not change.
       */
      cpe3.removeResource("com/google/gwt/user/client/Command.java");
      cpe3.removeResource("com/google/gwt/i18n/client/Constants.java");

      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertCollectionsConsistent(11);
      after.assertSameCollections(before);
    }

    {
      /*
       * Remove a unique resource, which will no longer be found. We also add a
       * new one to ensure that lack of size change doesn't confuse anything.
       * Consequently, the collections' identities should change.
       */
      cpe0.removeResource("com/google/gwt/i18n/client/Constants.java");
      cpe3.addResource("com/google/gwt/user/client/Window.java");

      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertCollectionsConsistent(11);
      after.assertNotSameCollections(before);
      after.assertPathIncluded("com/google/gwt/user/client/Window.java");
      after.assertPathNotIncluded("com/google/gwt/i18n/client/Constants.java");
    }
  }

  private void testResourceInCPE(TreeLogger logger, String resourceKey,
      ClassPathEntry expectedCPE, ClassPathEntry[] classPath,
      PathPrefix... pathPrefixes) {
    ResourceOracleImpl oracle = new ResourceOracleImpl(Arrays.asList(classPath));
    PathPrefixSet pps = new PathPrefixSet();
    for (PathPrefix pathPrefix : pathPrefixes) {
      pps.add(pathPrefix);
    }
    oracle.setPathPrefixes(pps);
    ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
    s.assertPathIncluded(resourceKey, expectedCPE);
  }

  private void testResourceModification(ClassPathEntry cpe1, ClassPathEntry cpe2) {
    TreeLogger logger = createTestTreeLogger();

    MOCK_CPE0 cpe0 = new MOCK_CPE0();
    MOCK_CPE3 cpe3 = new MOCK_CPE3();
    ResourceOracleImpl oracle = createResourceOracle(cpe0, cpe1, cpe2, cpe3);

    {
      /*
       * Baseline assumptions about the set of resources present by default.
       */
      ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
      s.assertPathIncluded("com/google/gwt/user/client/Command.java", cpe1);
      s.assertPathIncluded("com/google/gwt/user/client/Timer.java", cpe1);
    }

    // Add intentionally duplicate resources.
    cpe0.addResource("com/google/gwt/user/client/Command.java");
    cpe3.addResource("com/google/gwt/user/client/Timer.java");

    {
      /*
       * Ensure that the dups have the effect we expect.
       */
      ResourceOracleSnapshot s = refreshAndSnapshot(logger, oracle);
      s.assertPathIncluded("com/google/gwt/user/client/Command.java", cpe0);
      s.assertPathIncluded("com/google/gwt/user/client/Timer.java", cpe1);
    }

    {
      /*
       * Change a mock resource that was shadowed to ensure that the change
       * isn't observed.
       */
      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      before.assertCollectionsConsistent(10);

      cpe3.updateResource("com/google/gwt/user/client/Timer.java");

      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertSameCollections(before);
    }

    {
      /*
       * Change a mock resource that was not shadowed to ensure that the change
       * is observed.
       */
      ResourceOracleSnapshot before = new ResourceOracleSnapshot(oracle);
      before.assertCollectionsConsistent(10);

      cpe0.updateResource("com/google/gwt/user/client/Command.java");

      ResourceOracleSnapshot after = refreshAndSnapshot(logger, oracle);
      after.assertNotSameCollections(before);
    }
  }
}
