// $Id: TestUtil.java 17620 2009-10-04 19:19:28Z hardy.ferentschik $
/*
 * JBoss, Home of Professional Open Source Copyright 2009, Red Hat, Inc. and/or
 * its affiliates, and individual contributors by the @authors tag. See the
 * copyright.txt in the distribution for a full listing of individual
 * contributors.
 *
 * 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 org.hibernate.jsr303.tck.util;

import com.google.gwt.core.client.GWT;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.validation.Configuration;
import javax.validation.ConstraintViolation;
import javax.validation.MessageInterpolator;
import javax.validation.Path;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.Validation;
import javax.validation.bootstrap.GenericBootstrap;
import javax.validation.metadata.ConstraintDescriptor;
import javax.validation.metadata.ElementDescriptor;
import javax.validation.metadata.PropertyDescriptor;
import javax.validation.spi.ValidationProvider;

/**
 * Modified by Google.
 * <ul>
 * <li>Use RegExp instead of Pattern</li>
 * </ul>
 * @author Hardy Ferentschik
 */
public final class TestUtil {

  private static String VALIDATION_PROVIDER_TEST_CLASS = "validation.provider";

  private static ValidationProvider<?> validationProviderUnderTest;

  private TestUtil() {
  }

  public static Validator getValidatorUnderTest() {
    return getValidatorFactoryUnderTest().getValidator();
  }

  public static ValidationProvider<?> getValidationProviderUnderTest() {
    if (validationProviderUnderTest == null) {
      instantiateValidationProviderUnderTest();
    }
    return validationProviderUnderTest;
  }

  public static ValidatorFactory getValidatorFactoryUnderTest() {
    Configuration<?> config = getConfigurationUnderTest();
    return config.buildValidatorFactory();
  }

  public static Configuration<?> getConfigurationUnderTest() {
    if (validationProviderUnderTest == null) {
      instantiateValidationProviderUnderTest();
    }

    GenericBootstrap bootstrap = Validation.byDefaultProvider();
    return bootstrap.configure();
  }

  public static MessageInterpolator getDefaultMessageInterpolator() {
    Configuration<?> config = getConfigurationUnderTest();
    return config.getDefaultMessageInterpolator();
  }

  public static <T> void assertCorrectNumberOfViolations(
      Set<ConstraintViolation<T>> violations, int expectedViolations) {
    assertEquals(violations.size(), expectedViolations,
        "Wrong number of constraint violations. Expected: "
            + expectedViolations + " Actual: " + violations.size());
  }

  public static <T> void assertCorrectConstraintViolationMessages(
      Set<ConstraintViolation<T>> violations, String... messages) {
    List<String> actualMessages = new ArrayList<String>();
    for (ConstraintViolation<?> violation : violations) {
      actualMessages.add(violation.getMessage());
    }

    assertTrue(actualMessages.size() == messages.length,
        "Wrong number or error messages. Expected: " + messages.length
            + " Actual: " + actualMessages.size());

    for (String expectedMessage : messages) {
      assertTrue(actualMessages.contains(expectedMessage), "The message '"
          + expectedMessage
          + "' should have been in the list of actual messages: "
          + actualMessages);
      actualMessages.remove(expectedMessage);
    }
    assertTrue(actualMessages.isEmpty(),
        "Actual messages contained more messages as specified expected messages");
  }

  public static <T> void assertCorrectConstraintTypes(
      Set<ConstraintViolation<T>> violations,
      Class<?>... expectedConsraintTypes) {
    List<String> actualConstraintTypes = new ArrayList<String>();
    for (ConstraintViolation<?> violation : violations) {
      actualConstraintTypes.add(((Annotation) violation.getConstraintDescriptor().getAnnotation()).annotationType().getName());
    }

    assertEquals(expectedConsraintTypes.length, actualConstraintTypes.size(),
        "Wrong number of constraint types.");

    for (Class<?> expectedConstraintType : expectedConsraintTypes) {
      assertTrue(
          actualConstraintTypes.contains(expectedConstraintType.getName()),
          "The constraint type " + expectedConstraintType.getName()
              + " is not in the list of actual violated constraint types: "
              + actualConstraintTypes);
    }
  }

  public static <T> void assertCorrectPropertyPaths(
      Set<ConstraintViolation<T>> violations, String... propertyPaths) {
    List<Path> propertyPathsOfViolations = new ArrayList<Path>();
    for (ConstraintViolation<?> violation : violations) {
      propertyPathsOfViolations.add(violation.getPropertyPath());
    }

    assertEquals(propertyPaths.length, propertyPathsOfViolations.size(),
        "Wrong number of property paths. Expected: " + propertyPaths.length
            + " Actual: " + propertyPathsOfViolations.size());

    for (String propertyPath : propertyPaths) {
      Path expectedPath = PathImpl.createPathFromString(propertyPath);
      boolean containsPath = false;
      for (Path actualPath : propertyPathsOfViolations) {
        if (assertEqualPaths(expectedPath, actualPath)) {
          containsPath = true;
          break;
        }
      }
      if (!containsPath) {
        fail(expectedPath
            + " is not in the list of path instances contained in the actual constraint violations: "
            + propertyPathsOfViolations);
      }
    }
  }

  public static <T> void assertConstraintViolation(
      ConstraintViolation<T> violation, Class<?> rootBean, Object invalidValue,
      String propertyPath) {
    Path expectedPath = PathImpl.createPathFromString(propertyPath);
    if (!assertEqualPaths(violation.getPropertyPath(), expectedPath)) {
      fail("Property paths differ. Actual: " + violation.getPropertyPath()
          + " Expected: " + expectedPath);
    }

    assertEquals(violation.getRootBeanClass(), rootBean, "Wrong root bean.");
    assertEquals(violation.getInvalidValue(), invalidValue,
        "Wrong invalid value.");
  }

  public static boolean assertEqualPaths(Path p1, Path p2) {
    Iterator<Path.Node> p1Iterator = p1.iterator();
    Iterator<Path.Node> p2Iterator = p2.iterator();
    while (p1Iterator.hasNext()) {
      Path.Node p1Node = p1Iterator.next();
      if (!p2Iterator.hasNext()) {
        return false;
      }
      Path.Node p2Node = p2Iterator.next();

      // do the comparison on the node values
      if (p2Node.getName() == null) {
        if (p1Node.getName() != null) {
          return false;
        }
      } else if (!p2Node.getName().equals(p1Node.getName())) {
        return false;
      }

      if (p2Node.isInIterable() != p1Node.isInIterable()) {
        return false;
      }

      if (p2Node.getIndex() == null) {
        if (p1Node.getIndex() != null) {
          return false;
        }
      } else if (!p2Node.getIndex().equals(p1Node.getIndex())) {
        return false;
      }

      if (p2Node.getKey() == null) {
        if (p1Node.getKey() != null) {
          return false;
        }
      } else if (!p2Node.getKey().equals(p1Node.getKey())) {
        return false;
      }
    }

    return !p2Iterator.hasNext();
  }

  public static PropertyDescriptor getPropertyDescriptor(Class<?> clazz,
      String property) {
    Validator validator = getValidatorUnderTest();
    return validator.getConstraintsForClass(clazz).getConstraintsForProperty(
        property);
  }

  public static Set<ConstraintDescriptor<?>> getConstraintDescriptorsFor(
      Class<?> clazz, String property) {
    ElementDescriptor elementDescriptor = getPropertyDescriptor(clazz, property);
    return elementDescriptor.getConstraintDescriptors();
  }

  public static Object getInputStreamForPath(String path) {

    return null;
  }

  private static <U extends ValidationProvider<?>> void instantiateValidationProviderUnderTest() {
      validationProviderUnderTest = GWT.create(ValidationProvider.class);
  }

  public static class PathImpl implements Path {

    /**
     * Regular expression used to split a string path into its elements.
     *
     * @see <a href="http://www.regexplanet.com/simple/index.jsp">Regular
     *      expression tester</a>
     */
    private static final RegExp pathPattern = RegExp.compile("(\\w+)(\\[(\\w*)\\])?(\\.(.*))*");

    private static final String PROPERTY_PATH_SEPARATOR = ".";

    private final List<Node> nodeList;

    public static PathImpl createPathFromString(String propertyPath) {
      if (propertyPath == null) {
        throw new IllegalArgumentException(
            "null is not allowed as property path.");
      }

      if (propertyPath.length() == 0) {
        return createNewPath(null);
      }

      return parseProperty(propertyPath);
    }

    public static PathImpl createNewPath(String name) {
      PathImpl path = new PathImpl();
      NodeImpl node = new NodeImpl(name);
      path.addNode(node);
      return path;
    }

    private PathImpl() {
      nodeList = new ArrayList<Node>();
    }

    public void addNode(Node node) {
      nodeList.add(node);
    }

    public Iterator<Path.Node> iterator() {
      return nodeList.iterator();
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder();
      Iterator<Path.Node> iter = iterator();
      while (iter.hasNext()) {
        Node node = iter.next();
        builder.append(node.toString());
        if (iter.hasNext()) {
          builder.append(PROPERTY_PATH_SEPARATOR);
        }
      }
      return builder.toString();
    }

    private static PathImpl parseProperty(String property) {
      PathImpl path = new PathImpl();
      String tmp = property;
      do {
        MatchResult matcher = pathPattern.exec(tmp);
        if (matcher != null) {
          String value = matcher.getGroup(1);
          String indexed = matcher.getGroup(2);
          String index = matcher.getGroup(3);
          NodeImpl node = new NodeImpl(value);

          if (indexed != null && indexed.length() > 0) {
            node.setInIterable(true);
          }
          if (index != null && index.length() > 0) {
            try {
              Integer i = Integer.parseInt(index);
              node.setIndex(i);
            } catch (NumberFormatException e) {
              node.setKey(index);
            }
          }
          path.addNode(node);
          tmp = matcher.getGroup(5);
        } else {
          throw new IllegalArgumentException("Unable to parse property path "
              + property);
        }
      } while (tmp != null && tmp.length() > 0);
      return path;
    }
  }

  public static class NodeImpl implements Path.Node {

    private static final String INDEX_OPEN = "[";
    private static final String INDEX_CLOSE = "]";

    private final String name;
    private boolean isInIterable;
    private Integer index;
    private Object key;

    public NodeImpl(String name) {
      this.name = name;
    }

    NodeImpl(Path.Node node) {
      this.name = node.getName();
      this.isInIterable = node.isInIterable();
      this.index = node.getIndex();
      this.key = node.getKey();
    }

    public String getName() {
      return name;
    }

    public boolean isInIterable() {
      return isInIterable;
    }

    public void setInIterable(boolean inIterable) {
      isInIterable = inIterable;
    }

    public Integer getIndex() {
      return index;
    }

    public void setIndex(Integer index) {
      isInIterable = true;
      this.index = index;
    }

    public Object getKey() {
      return key;
    }

    public void setKey(Object key) {
      isInIterable = true;
      this.key = key;
    }

    @Override
    public String toString() {
      StringBuilder builder = new StringBuilder(name == null ? "" : name);
      if (isInIterable) {
        builder.append(INDEX_OPEN);
        if (getIndex() != null) {
          builder.append(getIndex());
        } else if (getKey() != null) {
          builder.append(getKey());
        }
        builder.append(INDEX_CLOSE);
      }
      return builder.toString();
    }
  }
}
