package detective.core.matcher;
import groovy.lang.Closure;
import java.util.ArrayList;
import java.util.List;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.junit.Assert;
import detective.core.dsl.DslException;
import detective.core.dsl.table.Row;
import detective.utils.GroovyUtils;
import detective.utils.TablePrinter;
/**
* Test if a type is a subset of other
*
* @author James Luo
*
*/
public class Subset<T> extends BaseMatcher<T> {
private final Object fullValue;
public Subset(T fullValue) {
this.fullValue = fullValue;
}
@Factory
public static <T> Matcher<T> subsetOf(T operand) {
return new Subset<T>(operand);
}
@Override
public boolean matches(Object subValue) {
try {
isMatches(fullValue, subValue);
} catch (AssertionError e) {
throw new SubsetAssertError(fullValue, subValue, e.getMessage(), e);
}
return true;
}
@Factory
public static <T> Matcher<T> subset(T operand) {
return new Subset<T>(operand);
}
@Override
public void describeTo(Description description) {
description.appendValue(fullValue);
}
private void isMatches(Object fullValue, Object subValue){
//List
if (fullValue instanceof List && subValue instanceof List){
final List<?> fulllList = (List<?>)fullValue;
final List<?> subList = (List<?>)subValue;
if (fulllList.size() == 0)
throw new java.lang.AssertionError("List can't be empty:" + fulllList);
if (subList.size() == 0)
throw new java.lang.AssertionError("List can't be empty:" + subList);
if (fulllList.size() < subList.size())
throw new java.lang.AssertionError("acutal data is smaller then expected list. actual item size:" + fulllList.size() + " expected item size:" + subList.size());
matchToList(fulllList, subList);
}else{
throw new java.lang.AssertionError("subset for now support only list to list. you types " + fullValue.getClass() + ":" + subValue.getClass());
}
}
private void matchToList(final List<?> fulllList, final List<?> subList) throws AssertionError {
//The first column is the key, which identify how to identify a row
//it can be a row number (if it's a number), or it's a closure which return a boolean to identify a row
for (Object expectedItem : subList){
if (expectedItem instanceof Row){
//we support row/table first
Row row = (Row)expectedItem;
Object actualRowFound = findRightRowInFullList(fulllList, row);
if (actualRowFound == null){
throw new java.lang.AssertionError("couldn't found any item in actual list based on first column of row" + row);
}
compareTwoRow(actualRowFound, row);
}else{
throw new java.lang.AssertionError("Subset have to be a Row type, type you provided: " + expectedItem.getClass().getName());
}
}
}
private void compareTwoRow(Object actualRow, Row expectedRow) throws AssertionError{
String[] headers = expectedRow.getHeaderAsStrings();
for (int i = 1; i < headers.length; i++){
String propertyName = headers[i];
Object actualProperty = this.getPropertyInner(actualRow, propertyName);
try {
Assert.assertThat(actualProperty, IsEqual.equalTo(expectedRow.getProperty(propertyName)));
} catch (AssertionError e) {
throw new AssertionError(buildDetailErrorMsg(i, propertyName, actualRow, expectedRow, e.getMessage()), e);
}
}
}
private String buildDetailErrorMsg(int columnIndex, String columnName, Object actualRow, Row expectedRow, String originMsg){
StringBuilder sb = new StringBuilder(originMsg);
sb.append(" on column [").append(columnIndex).append("] with name [").append(columnName).append("]");
List<Object> actualList = new ArrayList<Object>();
actualList.add(actualRow);
sb.append(TablePrinter.printObjectAsTable(actualList, "Actual row"));
List<Object> expectedList = new ArrayList<Object>();
expectedList.add(expectedRow);
sb.append(TablePrinter.printObjectAsTable(expectedList, "Expected row"));
return sb.toString();
}
private Object findRightRowInFullList(final List<?> fullList, Row row)
throws AssertionError {
String[] headers = row.getHeaderAsStrings();
final Object firstColumn = getPropertyInner(row, headers[0]);
Closure<Object> rowSearcher = null;
if (firstColumn instanceof Number){
//Number
Integer intIndex = ((Number)firstColumn).intValue();
if (intIndex < 0 || intIndex >= fullList.size())
throw new DslException("Row index number have to in right range, in your case the number have to between 0 and " + (fullList.size() - 1) + "");
rowSearcher = new Closure<Object>(this, fullList){
public Object call() {
return fullList.get(((Number)firstColumn).intValue());
}
};
}else if (firstColumn instanceof Closure){
//A closure which return boolean
final Closure booleanSearch = (Closure)firstColumn;
rowSearcher = new Closure<Object>(this, fullList){
public Object call() {
for (Object item : fullList){
Object resultFromBooleanSearch = booleanSearch.call(item);
if (resultFromBooleanSearch instanceof Boolean){
if (Boolean.TRUE.equals(resultFromBooleanSearch))
return item;
}
}
return null;
}
};
}else{
throw new java.lang.AssertionError("the first column of your table used for located a row when compare between two tables, this column should either a number which identify the row index, or a search expression, please see document for more information.");
}
Object actualRowFound = rowSearcher.call();
return actualRowFound;
}
private Object getPropertyInner(Object object, String propertyName){
return GroovyUtils.getProperty(object, propertyName);
}
}