/*
 * 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.user.rebind.ui;

import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.util.UnitTestTreeLogger;
import com.google.gwt.user.client.ui.ImageBundle.Resource;
import com.google.gwt.user.rebind.ui.ImageBundleBuilder.HasRect;
import com.google.gwt.user.rebind.ui.ImageBundleGenerator.JMethodOracle;

import junit.framework.TestCase;

import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Unit tests for {@link ImageBundleGenerator}. Note that, at present, only a
 * small minority of the functionality is really covered. The present tests
 * cover the correctness of compile-time diagnostics and the sequence and
 * phrasing of error and warning log messages.
 */
public class ImageBundleGeneratorTest extends TestCase {

  private static class MockRect implements HasRect {
    private final String name;
    private final int width, height;
    private int left, top;
    private boolean hasBeenPositioned;

    MockRect(String name, int width, int height) {
      this.name = name;
      this.width = width;
      this.height = height;
    }

    @Override
    public int getHeight() {
      return height;
    }

    @Override
    public int getLeft() {
      return left;
    }

    @Override
    public String getName() {
      return name;
    }

    @Override
    public int getTop() {
      return top;
    }

    @Override
    public int getWidth() {
      return width;
    }

    @Override
    public boolean hasBeenPositioned() {
      return hasBeenPositioned;
    }

    @Override
    public void setPosition(int left, int top) {
      this.left = left;
      this.top = top;
      hasBeenPositioned = true;
    }

    @Override
    public String toString() {
      return "{" + left + ", " + top + ", " + width + ", " + height + "}";
    }

    void assertPosition(int left, int top) {
      assertTrue(hasBeenPositioned);
      assertEquals(left, this.left);
      assertEquals(top, this.top);
    }

    MockRect duplicate() {
      final MockRect mr = new MockRect(name, width, height);
      mr.hasBeenPositioned = hasBeenPositioned;
      mr.left = left;
      mr.top = top;
      return mr;
    }
  }

  /**
   * Ensures that two collections of MockRects are arranged identically.
   * 
   * @param a first collection
   * @param b second collection
   */
  private static void assertSameArrangement(Collection<MockRect> a,
      Collection<MockRect> b) {
    final Map<String, MockRect> index = new HashMap<String, MockRect>();
    for (MockRect r : a) {
      index.put(r.getName(), r);
    }

    for (MockRect rb : b) {
      final MockRect ra = index.get(rb.getName());
      rb.assertPosition(ra.getLeft(), ra.getTop());
    }
  }

  @Resource("dunebuggy.gif")
  public void duneBuggyInDefaultPackage() {
  }

  @Resource("a/b/c/dunebuggy.gif")
  public void duneBuggyInNonDefaultPackage() {
  }

  @Resource("io.jpg")
  public void ioInSamePackage() {
  }

  /**
   * Validates that presenting the images in different orders does not affect
   * the arrangement.
   */
  public void testArrangementIsDeterministic() {

    List<MockRect> orderA = Arrays.asList(new MockRect("a", 50, 100),
        new MockRect("b", 50, 100), new MockRect("c", 100, 50), new MockRect(
            "d", 50, 50), new MockRect("e", 50, 50));
    List<MockRect> orderB = new ArrayList<MockRect>();
    for (int i = orderA.size() - 1; i <= 0; --i) {
      orderB.add(orderA.get(i));
    }

    ImageBundleBuilder.arrangeImages(orderA);
    ImageBundleBuilder.arrangeImages(orderB);

    assertSameArrangement(orderA, orderB);
  }

  /**
   * Ensures that the basic image packing algorithm is arranging images as
   * expected.
   */
  public void testBasicImageArrangement() {
    // Expected in 2nd column, 2nd in 2nd row.
    final MockRect ra = new MockRect("a", 20, 30);
    // Expected to be 1st column.
    final MockRect rb = new MockRect("b", 10, 100);
    // Expected in 2nd column, 1st in 2nd row.
    final MockRect rc = new MockRect("c", 20, 40);
    // Expected in 2nd column, it is the 1st row.
    final MockRect rd = new MockRect("d", 100, 60);
    // Expected in 2nd column, 3rd in 2nd row.
    final MockRect re = new MockRect("e", 10, 30);

    final Collection<MockRect> rects = new ArrayList<MockRect>();
    Collections.addAll(rects, ra, rb, rc, rd, re);
    ImageBundleBuilder.arrangeImages(rects);

    ra.assertPosition(30, 60);
    rb.assertPosition(0, 0);
    rc.assertPosition(10, 60);
    rd.assertPosition(10, 0);
    re.assertPosition(50, 60);
  }

  /**
   * This method is to remind the reader that ClassLoader.getResource() doesn't
   * want a leading "/" on resource names.
   */
  public void testClassLoaderGetResourceHatesLeadingSlash() {
    String fqcn = getClass().getCanonicalName();
    // This class has a name.
    assertNotNull(fqcn);
    String thisClassResName = fqcn.replace('.', '/') + ".class";
    // It has some slashes.
    assertTrue(thisClassResName.indexOf('/') >= 0);
    // But it does not start with a slash.
    assertFalse(thisClassResName.startsWith("/"));
    // The resource is successfully found without a leading slash.
    URL url = getClass().getClassLoader().getResource(thisClassResName);
    assertNotNull(url);
    // It fails to be found found with a leading slash.
    url = getClass().getClassLoader().getResource("/" + thisClassResName);
    assertNull(url);
  }

  /**
   * Tests that a message is logged and an exception is thrown when a resource
   * isn't found after being sought based on the ImageBundle.Resource
   * annotation.
   */
  @Resource("notfound.png")
  public void testResourceNotFoundGivenAnnotation() {
    UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
    b.expectError(
        ImageBundleGenerator.msgCannotFindImageFromMetaData("from/metadata/notfound.png"),
        null);
    UnitTestTreeLogger logger = b.createLogger();

    try {
      getImageName(logger, new String[0], "nonexistentImg", "from.metadata",
          getResourceAnnotation("testResourceNotFoundGivenAnnotation"));
      fail("Should have thrown");
    } catch (UnableToCompleteException e) {
    }
    logger.assertCorrectLogEntries();
  }

  /**
   * Tests that a message is logged and an exception is thrown when a resource
   * isn't found after being sought based on the method name.
   */
  public void testResourceNotFoundGivenNoMetaData() {
    UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
    b.expectError(ImageBundleGenerator.MSG_NO_FILE_BASED_ON_METHOD_NAME, null);
    b.expectError("test/nonexistentImg.png", null);
    b.expectError("test/nonexistentImg.gif", null);
    b.expectError("test/nonexistentImg.jpg", null);
    UnitTestTreeLogger logger = b.createLogger();

    try {
      getImageName(logger, new String[0], "nonexistentImg", "test", null);
      fail("Should have thrown");
    } catch (UnableToCompleteException e) {
    }
    logger.assertCorrectLogEntries();
  }

  /**
   * Tests that resources can be found in a variety of ways from an image bundle
   * residing in the default package.
   */
  public void testResourcesFoundFromImageBundleInDefaultPackage()
      throws UnableToCompleteException {
    UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
    UnitTestTreeLogger logger = b.createLogger();

    {
      // [1] Find an image in the default package using the method name.
      String imgName = getImageName(logger, new String[] {
          "cabbage.jpg", "lettuce.jpg",}, "cabbage", "", null);
      assertEquals("cabbage.jpg", imgName);
    }

    {
      // [2] Find an image in the default package using an annotation.
      String imgName = getImageName(logger, new String[] {
          "car.png", "dunebuggy.gif",}, "vehicleImage", "",
          getResourceAnnotation("duneBuggyInDefaultPackage"));
      assertEquals("dunebuggy.gif", imgName);
    }
    {
      // [3] Find an image in a non-default package using an annotation.
      String imgName = getImageName(logger, new String[] {
          "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage",
          "", getResourceAnnotation("duneBuggyInNonDefaultPackage"));
      assertEquals("a/b/c/dunebuggy.gif", imgName);
    }

    logger.assertCorrectLogEntries();
  }

  /**
   * Tests that resources can be found in a variety of ways from an image bundle
   * residing in a non-default package.
   */
  public void testResourcesFoundFromImageBundleInNonDefaultPackage()
      throws UnableToCompleteException {
    UnitTestTreeLogger.Builder b = new UnitTestTreeLogger.Builder();
    UnitTestTreeLogger logger = b.createLogger();

    {
      // [1] Find an image in the same package using the method name.
      String imgName = getImageName(logger, new String[] {
          "x/y/z/europa.png", "x/y/z/io.jpg",}, "io", "x.y.z", null);
      assertEquals("x/y/z/io.jpg", imgName);
    }

    {
      // [2] Find an image in the same package using an annotation.
      String imgName = getImageName(logger, new String[] {
          "x/y/z/europa.png", "x/y/z/io.jpg",}, "moonImage", "x.y.z",
          getResourceAnnotation("ioInSamePackage"));
      assertEquals("x/y/z/io.jpg", imgName);
    }

    {
      // [3] Find an image in a non-default package using an annotation.
      String imgName = getImageName(logger, new String[] {
          "car.png", "dunebuggy.gif", "a/b/c/dunebuggy.gif",}, "vehicleImage",
          "x.y.z", getResourceAnnotation("duneBuggyInNonDefaultPackage"));
      assertEquals("a/b/c/dunebuggy.gif", imgName);
    }

    logger.assertCorrectLogEntries();
  }

  private JMethodOracle createJMethodOracle(final String methodName,
      final String packageName, final Resource resourceAnnotation) {
    return new JMethodOracle() {
      @Override
      public Resource getAnnotation(Class<Resource> clazz) {
        return resourceAnnotation;
      }

      @Override
      public String getName() {
        return methodName;
      }

      @Override
      public String getPackageName() {
        return packageName;
      }
    };
  }

  private String getImageName(UnitTestTreeLogger logger,
      final String[] pretendResources, String methodName, String pkgName,
      final Resource resAnn) throws UnableToCompleteException {
    ImageBundleGenerator ibg = new ImageBundleGenerator(
        new ImageBundleGenerator.ResourceLocator() {
          private final List<String> resList = Arrays.asList(pretendResources);

          @Override
          public boolean isResourcePresent(String resName) {
            return resList.contains(resName);
          }
        });
    JMethodOracle methodOracle = createJMethodOracle(methodName, pkgName,
        resAnn);
    return ibg.getImageResourceName(logger, methodOracle);
  }

  private Resource getResourceAnnotation(String methodName) {
    Throwable caught = null;
    try {
      Resource res = getClass().getMethod(methodName, new Class[0]).getAnnotation(
          Resource.class);
      assertNotNull(res);
      return res;
    } catch (SecurityException e) {
      caught = e;
    } catch (NoSuchMethodException e) {
      caught = e;
    }
    fail("Unable to get @Resource annotation from method '" + methodName
        + "' during test due to exception: " + caught.getMessage());
    return null;
  }
}
