package net.contrapunctus.rngzip;

import java.io.*;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.TreeSet;
import net.contrapunctus.rngzip.io.RNGZInputStream;
import net.contrapunctus.rngzip.io.RNGZOutputStream;
import net.contrapunctus.rngzip.io.RNGZSettings;
import net.contrapunctus.rngzip.util.BaliAutomaton;
import net.contrapunctus.rngzip.util.ErrorReporter;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.kohsuke.bali.automaton.TreeAutomaton;
import org.kohsuke.bali.writer.*;
import org.xml.sax.Attributes;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;
import runtime.ValidateletImpl;

/**
 * Test suite for a generic compress-decompress round-trip, using
 * files in tests/cases/ sub-directory.  Not only do we look for
 * exceptions and assertion failures in the process, but we ensure
 * that the ultimate output is identical (in terms of SAX events) to
 * the original input.
 */
@RunWith(Parameterized.class)
public class GenericTest 
{
  public static final String TEST_DIR = "tests/cases";

  private XMLReader xmlReader;
  private ErrorReporter errorReporter;
  private ByteArrayOutputStream errorBytes;
  private PrintStream errorStream;
  private RNGZSettings settings;

  private String origFileName;    // ---.---.xml
  private String schemaFileName;  // ---.rng
  private byte[] compressedBytes;
  private EventRecorder origSax, newSax;

  @Before
  public void setup() throws Exception 
  {
    errorBytes = new ByteArrayOutputStream();
    errorStream = new PrintStream(errorBytes);
    errorReporter = new ErrorReporter(errorStream);
    xmlReader = XMLReaderFactory.createXMLReader();
    xmlReader.setErrorHandler(errorReporter);
    settings = new RNGZSettings();
  }

  @Parameterized.Parameters 
  public static LinkedList<String[]> cases() 
  {
    File testdir = new File(TEST_DIR);
    String[] tests = testdir.list(new FilenameFilter() {
        public boolean accept(File dir, String name) {
          return name.matches(".*\\..*\\.xml");
        }
      });
    LinkedList<String[]> args = new LinkedList<String[]>();
    for(String t : tests) 
      {
        args.add( new String[] { t } );
      }
    return args;
  }

  public GenericTest(String name)
  {
    origFileName = TEST_DIR + File.separatorChar + name;
    schemaFileName = origFileName.replaceFirst("\\..*\\.xml", ".rng");
  }

  @Test
  public void roundTrip() throws Exception
  {
    try
      {
        validateOnly();
        recordOriginal();
        compress();
        decompress();
        origSax.assertEqual(newSax);
        // If we get here, there were no exceptions.
        // But was there any output on the stream?
        errorStream.flush();
        if( errorBytes.size() > 0 )
          throw new Exception("error stream");
        if( System.getProperty("DEBUG_TEST_CASES") != null ) {
          maybeSaveRNZ();
          maybeSaveNewXML();
        }
      }
    catch( Throwable th )
      {
        maybeSaveRNZ();
        maybeSaveNewXML();
        String msg = composeErrorMessage(th.getMessage());
        throw new Error( msg, th );
      }
  }

  private void validateOnly() throws Exception
  {
    AutomatonWriter writer = new NullWriter();    
    Interpreter interpreter = new Interpreter();
    writer = new MultiWriter(writer, interpreter);
    BaliAutomaton automaton = BaliAutomaton.fromRNG(new File(schemaFileName));
    automaton.writeTo(writer);
    ValidateletImpl v = interpreter.createValidatelet();
    xmlReader.setContentHandler(v);
    try {
      xmlReader.parse(origFileName);
    }
    catch( SAXParseException exn ) {
      errorStream.println("WARNING: Bali COULD NOT VALIDATE THIS TEST CASE");
    }
  }

  private void recordOriginal() throws Exception
  {
    origSax = new EventRecorder();
    xmlReader.setContentHandler(origSax);
    xmlReader.parse(origFileName);
  }

  private void compress() throws Exception
  {
    ByteArrayOutputStream bo = new ByteArrayOutputStream();
    RNGZOutputStream ro = new RNGZOutputStream(bo, settings, null);
    GenericCompressor gc = new GenericCompressor
      (schemaFileName, errorReporter, ro);
    xmlReader.setContentHandler(gc);
    xmlReader.parse(origFileName);
    ro.close();
    compressedBytes = bo.toByteArray();
  }

  private void decompress() throws Exception
  {
    ByteArrayInputStream bi = new ByteArrayInputStream(compressedBytes);
    RNGZInputStream ri = new RNGZInputStream(bi, settings);
    newSax = new EventRecorder();
    new GenericDecompressor(schemaFileName, ri, newSax);
    ri.close();
  }

  private void maybeSaveRNZ() throws Exception
  {
    if( compressedBytes != null )
      {
        String rnz = origFileName.replaceFirst("\\.xml$", ".rnz");
        FileOutputStream fos = new FileOutputStream(rnz);
        fos.write(compressedBytes);
        fos.close();
        errorStream.println("Saved " + rnz + " (" +
                            compressedBytes.length +
                            " bytes)");
      }
  }

  private void maybeSaveNewXML() throws IOException
  {
    if( newSax != null )
      {
        String xin = origFileName.replaceFirst("\\.xml$", ".xin");
        String xout = origFileName.replaceFirst("\\.xml$", ".xout");
        origSax.saveTo(xin);
        newSax.saveTo(xout);
        errorStream.println("Saved " + xin + " and " + xout);
      }
  }

  private String composeErrorMessage( String exnMesg )
  {
    errorStream.close();
    StringBuilder buf = new StringBuilder();
    buf.append(exnMesg);
    buf.append("\nFailure in ");
    buf.append(origFileName);
    buf.append("\n");
    if( errorBytes.size() > 0 )
      {
        buf.append("Details follow:\n");
        buf.append(new String(errorBytes.toByteArray()));
      }
    return buf.toString();
  }

  /**
   * To compare the input against the output, we don't do it at the
   * character level; there are too many variations possible: spacing
   * and indentation, <foo/> vs. <foo></foo>, id="3" vs. id='3', etc.
   * So this ContentHandler will accumulate a history of events, for
   * comparison to another history.
   */
  static class EventRecorder extends DefaultHandler
  {
    /* We store events as strings, the first character indicating the
     * kind of event, so that the document
     *    <foo id='78' href='abc'>Yes!<bar/>No</foo>
     * becomes
     *
     *   "+foo", "@href", "=abc",     // attributes in alphabetical order
     *   "@id", "=78", "$Yes!",
     *   "+bar", "-bar", "$No", "-foo"
     * 
     * Character events made entirely of white space are assumed to be
     * ignorable, and consecutive character events are merged.
     */
    private LinkedList<String> history = new LinkedList<String>();
    private TreeSet<String> keys = new TreeSet<String>();

    public void startElement( String ns, String ln, String qn,
                              Attributes at )
    {
      history.add("+" + qn);
      /* We can't count on attributes being in alpha order,
         so add keys to a sorted set. */
      keys.clear();
      for( int i = 0;  i < at.getLength();  i++ )
        {
          keys.add( at.getQName(i) );
        }
      for( String k : keys )
        {
          history.add("@" + k);
          history.add("=" + at.getValue(k));
        }
    }

    public void endElement( String ns, String ln, String qn )
    {
      history.add("-" + qn);
    }

    public void characters( char[] ch, int start, int len )
    {
      /* Check whether it's all white space. */
      boolean ignoreable = true;
      for( int i = 0;  i < len; i++ )
        {
          if( ! Character.isWhitespace(ch[start+i]) )
            {
              ignoreable = false;
              break;
            }
        }
      if( ignoreable ) return;
      String txt = new String(ch, start, len);
      /* Check whether last event was also a string. */
      String last = history.removeLast();
      if( last.charAt(0) == '$' )
        { /* Merge consecutive strings. */
          history.add(last + txt);
        }
      else
        {
          history.add(last);
          history.add('$' + txt);
        }
    }

    public int size()
    {
      return history.size();
    }

    public void saveTo( String fileName ) throws IOException
    {
      PrintStream ps = new PrintStream(new FileOutputStream(fileName), false, "UTF-8");
      for( String e : history )
        {
          ps.println(e);
        }
      ps.close();
    }

    /* Check whether two event histories are the same. */
    public void assertEqual( EventRecorder that )
    {
      Iterator<String> i = this.history.iterator();
      Iterator<String> j = that.history.iterator();
      while( i.hasNext() && j.hasNext() )
        {
          String si = i.next();
          String sj = j.next();
          assert si.equals(sj) : "event mismatch: ["+si+"]["+sj+"]";
        }
      assert i.hasNext() == j.hasNext() 
        : "one event history shorter than other";
    }
  }

  /* A little test program for the EventRecorder. */
  public static void main(String[] args) throws Exception
  {
    XMLReader xr = XMLReaderFactory.createXMLReader();
    EventRecorder er1 = new EventRecorder();
    EventRecorder er2 = new EventRecorder();
    xr.setContentHandler(er1);
    xr.parse(args[0]);
    xr.setContentHandler(er2);
    xr.parse(args[1]);
    er1.assertEqual(er2);
  }
}

