/*
 * @(#)$Id: DataTypeTester.java 1650 2005-03-31 23:19:14Z kohsuke $
 *
 * Copyright 2001 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the proprietary information of Sun Microsystems, Inc.  
 * Use is subject to license terms.
 * 
 */
/*
 * $Id: DataTypeTester.java 1650 2005-03-31 23:19:14Z kohsuke $
*/
package com.sun.msv.datatype.xsd.conformance;

import com.sun.msv.datatype.xsd.DatatypeFactory;
import com.sun.msv.datatype.xsd.TypeIncubator;
import com.sun.msv.datatype.xsd.XSDatatype;
import com.sun.msv.datatype.xsd.XSDatatypeImpl;
import org.jdom.Element;
import org.relaxng.datatype.DatatypeException;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.util.List;

/**
 * tests DataType.verify method with various types and various lexical values.
 * 
 * @author <a href="mailto:kohsuke.kawaguchi@eng.sun.com">Kohsuke KAWAGUCHI</a>
 */
public class DataTypeTester
{
    /** progress indication will be sent to this object */
    private final PrintStream out;
    
    /** error will be passed to this object */
    private final ErrorReceiver err;

    public DataTypeTester( PrintStream out, ErrorReceiver err )
    {
        this.out = out;
        this.err = err;
    }
    
    public void run( Element testCase )
        throws Exception
    {
        out.println( testCase.getAttributeValue("name") );
        
        String[] values;
        {// read values
            List lst = testCase.getChildren("value");
            values = new String[lst.size()];
            for( int i=0; i<values.length; i++ )
                values[i] = ((Element)lst.get(i)).getText();
        }
        
        String[] wrongValues;
        {// read wrongValues, which is always wrong with any types tested
            Element wrongs = testCase.getChild("wrongs");
            if(wrongs==null)    throw new Exception("no <wrongs>");
            
            List lst = wrongs.getChildren("value");
            wrongValues = new String[lst.size()];
            for( int i=0; i<wrongValues.length; i++ )
                wrongValues[i] = ((Element)lst.get(i)).getText();
        }
        
        // parses a test pattern, which basically enumerates
        // possible test pattern and its expected result
        Element facetElement = testCase.getChild("facets");
        TestPattern pattern =
            TestPatternGenerator.parse(
                (Element)facetElement.getChildren().get(0));
        
        {
            List lst = testCase.getChildren("answer");
            for( int i=0; i<lst.size(); i++ )
            {
                Element item = (Element)lst.get(i);
                
                // perform test as a single type
                XSDatatypeImpl t = (XSDatatypeImpl)DatatypeFactory.getTypeByName(item.getAttributeValue("for"));
                if(t==null)
                {
                    System.out.println(item.getAttributeValue("for") + " is undefined type");
                }
                testDataType(
                    t,
                    values, wrongValues,
                    TestPatternGenerator.trimAnswer(item.getText()),
                    pattern,
                        // wrap it by intrisic restriction due to this datatype
                    false
                    );

// TODO : we need more systematic approach here
                // perform test as an union type (completeness only)
//                DataType u = DataTypeFactory.deriveByUnion(null,
//                    new DataType[]{ t, getRandomType(), getRandomType() } );
//                testDataType(
//                    u, values, new String[0]/*no explicitly wrong values to test*/,
//                    new BaseAnswerWrapper(item.getText(), pattern, true ),
//                    true );
            }
        }
    }
    
    /**
     * tests one datatype
     *
     * @param typeObj
     *        DataType object to be tested
     * @param answer
     *        default answer sheet for values parameter.
     *        each "o" indicates valid, whereas "." indicates invalid.
     *        answer.charAt(i) indicates validity of values[i],
     * @param values
     *        possibly valid values. These values are tested against
     *        DataType object.
     * @param wrongs
     *        values which are always wrong. But they are tested anyway.
     * @param pattern
     *        test pattern generator that iterates a combination of facets
     *        and its predicted result.
     * @param completenessOnly
     *        true indicates test is only performed for completeness; that is,
     *        make sure that answer is 'o' for those which is marked as 'o'.
     *        this flag is used to test union types.
     */
    public void testDataType(
        XSDatatypeImpl baseType,
        String[] values, String[] wrongs,
        String baseAnswer, TestPattern pattern,
        boolean completenessOnly )
        throws Exception
    {
        long startTime = System.currentTimeMillis();
        out.println("  testing " + baseType.getName() +
            " (total "+pattern.totalCases()+" patterns)" );
        
        pattern.reset();
        
        long cnt=0;
        
        while(true)
        {
            if((cnt%500)==0 && cnt!=0 )    out.print("\r"+cnt+"    ");
            
            if( !pattern.hasMore() )    break;
            final TypeIncubator ti = new TypeIncubator(baseType);
            String answer;
            
            try
            {
                answer = TestPatternGenerator.merge( baseAnswer, pattern.get(ti), true );
            }
            catch( DatatypeException bte )
            {// ignore this error
                pattern.next();
                continue;
            } 
            
            // derive a type with test facets.
            XSDatatype typeObj=null;
            try
            {
                typeObj = ti.derive("anonymousURI","anonymousLocal");
            }
            catch( DatatypeException bte )
            {
                err.reportTestCaseError(baseType,ti,bte);
            }

            {// make sure that the serialization works.
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                ObjectOutputStream oos = new ObjectOutputStream(bos);
        
                // serialize it
                oos.writeObject( typeObj );
                oos.flush();
                
                // obtain byte array (just in case)
                ObjectInputStream ois = new ObjectInputStream(
                    new ByteArrayInputStream(bos.toByteArray()));
                typeObj = (XSDatatype)ois.readObject();
                ois.close();
            }
            
            if(typeObj!=null)
            {
                if( answer.length()!=values.length )
                    throw new IllegalStateException("answer and values have different length");

//                if( testCase.facets.isEmpty())    out.println("nofacet");
//                else    testCase.facets.dump(out);

                // test each value and see what happens
                for( int i=0; i<values.length; i++ )
                {
                    boolean v = typeObj.isValid(values[i],DummyContextProvider.theInstance);
                    boolean d;
                    
                    boolean roundTripError = false;
                    
                    Object o = typeObj.createValue(
                        values[i],DummyContextProvider.theInstance);
                    
                    try {
                        if(o!=null) {
                            // should be able to convert it back.
                            String s = typeObj.convertToLexicalValue(o,DummyContextProvider.theInstance);
                            // try round trip conversion.
                            Object o2 = typeObj.createValue(s,DummyContextProvider.theInstance);
                            if( o2==null || !o.equals(o2) )
                                roundTripError = true;
                        }
                    } catch( UnsupportedOperationException uoe ) {
                        // ignore this exception
                    } catch( IllegalArgumentException iae ) {
                        roundTripError = true;
                    }

                    Object jo = typeObj.createJavaObject( values[i], DummyContextProvider.theInstance );
                    
                    if( jo!=null ) {
                        if( !typeObj.getJavaObjectType().isAssignableFrom(jo.getClass()) ) {
                            System.out.println("type error");
                            roundTripError = true;
                        }
                        
                        String s = typeObj.serializeJavaObject( jo,
                                        DummyContextProvider.theInstance );
                        if(s==null) {
                            System.out.println("serializeJavaObject failed");
                            roundTripError = true;
                        } else {
                            Object o2 = typeObj.createJavaObject(s,DummyContextProvider.theInstance);
                            if( o2==null ) {
                                System.out.println("round-trip conversion failed");
                                roundTripError = true;
                            }
                        }
                    }
                    
                    try {
                        typeObj.checkValid(values[i],DummyContextProvider.theInstance);
                        d = true;
                    } catch( DatatypeException de ) {
                        d = false;
                    }
                    
                    // if convertToValueObject and verify method differs,
                    // it's always an error.
                    // roundTripError is always an error.
                    if(!roundTripError && (o!=null)==v && (jo!=null)==v ) {
                        
                        if(v && d && answer.charAt(i)=='o')
                            continue;    // as predicted
                        if(!v && !d && answer.charAt(i)=='.')
                            continue;    // as predicted
                    
                        if(completenessOnly && answer.charAt(i)=='.' && v==d )
                            continue;    // do not report error if
                                        // the validator accepts things that
                                        // may not be accepted.
                    }
                    
                    // dump error messages
                    if( !err.report( new UnexpectedResultException(
                            typeObj, baseType.getName(),
                            values[i], answer.charAt(i)=='o',
                            ti ) ) )
                    {
                        out.println("test aborted");
                        return;
                    }
                }

                // test each wrong values and makes sure that they are rejected.
                for( int i=0; i<wrongs.length; i++ ) {
                    boolean err = false;
                    
                    err = typeObj.isValid(wrongs[i],DummyContextProvider.theInstance);
                    try {
                        typeObj.checkValid(wrongs[i],DummyContextProvider.theInstance);
                        err = true;
                    } catch (DatatypeException de) {
                        ;    // it should throw an exception
                    }
                    
                    if( typeObj.createJavaObject(wrongs[i],DummyContextProvider.theInstance)!=null )
                        err = true;
                    
                    if( err ) {
                        if( !this.err.report( new UnexpectedResultException(
                            typeObj, baseType.getName(),
                            wrongs[i], false, ti ) ) )
                        {
                            out.println("test aborted");
                            return;
                        }
                    }
                }
            }
            
            cnt++;
            pattern.next();
        }
        
        out.println();
        // test done
        out.println("  " + cnt + " cases tested ("+(System.currentTimeMillis()-startTime)+"ms)");
    }
    
    public static final String[] builtinTypesList =
    new String[]{
        "string",
        "boolean",
        "number",
        "float",
        "double",
        "duration",
        "dateTime",
        "time",
        "date",
        "gYearMonth",
        "gYear",
        "gMonthDay",
        "gDay",
        "gMonth",
        "hexBinary",
        "base64Binary",
        "anyURI",
        "ID",
        "IDREF",
        "ENTITY",
        "QName",
        "normalizedString",
        "token",
        "language",
        "IDREFS",
        "ENTITIES",
        "NMTOKEN",
        "NMTOKENS",
        "Name",
        "NCName",
        "NOTATION",
        "integer",
        "nonPositiveInteger",
        "negativeInteger",
        "long",
        "int",
        "short",
        "byte",
        "nonNegativeInteger",
        "unsignedLong",
        "unsignedInt",
        "unsignedShort",
        "unsignedByte",
        "positiveInteger"
    };
}
