/*
 * LpParserTest.java
 * JUnit based test
 *
 * Copyright (C) 2006 - 2007 Martin Slota
 *
 * This program is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation; either version 2 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/*
 * History:
 * v0.1 (2007-01-03): initial version
 * v0.2 (2007-01-05):
 * - testPropositionalRule, testNormalRule, testBothRules, testExceptions and
 *   testRulesFromFile added
 * - documentation added
 * v0.2.1 (2007-01-06):
 * - testConstructorExceptions added
 * v0.2.2 (2007-01-12):
 * - testConstructorExceptions modified to expect IllegalArgumentException
 *   instead of NullPointerException
 * - assertion messages added
 * v0.2.3 (2007-01-28):
 * - unnecessary throws clauses removed
 * - documentation updated
 * v0.2.4 (2007-01-28):
 * - unnecessary throws clauses removed
 * - documentation updated
 * v0.2.5 (2007-02-08):
 * - changed because LpParser changed (made reusable...)
 * - refactored (testConstructorExceptions changed to testSetInputExceptions)
 * v0.2.6 (2007-02-11):
 * - some changes due to the addition of the LpAtom class
 * - testInvalidRule added
 * v0.2.7 (2007-02-12):
 * - changed because of changes in LpParser
 * v0.2.8 (2007-03-05):
 * - tests made reusable by EvolpParserTest and DlpParserTest
 * v1.0.0 (2007-05-05):
 * - promoted to version 1.0.0 :o)
 *
 * PENDING write more tests (parseTerm, parseAtom, parseLiteral...)
 */

package lp.parse;

import junit.framework.*;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import lp.struct.LpAtom;
import lp.struct.LpConstant;
import lp.struct.LpLiteral;
import lp.struct.LpPredicate;
import lp.struct.LpRule;
import lp.struct.LpTerm;
import lp.struct.LpVariable;

import lp.test.util.PartReader;
import lp.test.util.TabSeparatedReader;

import lp.unit.LogicProgram;

import static lp.parse.LpTokenType.*;
import lp.struct.LpCompoundTerm;
import lp.struct.LpFunction;
import lp.struct.util.LpBuffer;
import lp.struct.util.LpPrettyPrinter;

/**
 * Contains tests of the {@link LpParser} class.
 *
 * @author Martin Slota
 * @version 1.0.0
 * @see LpParser
 */
public class LpParserTest extends TestCase {
	/**
	 * A container for the rule constructed by {@link #getRule1()}.
	 */
	private LpRule rule1 = null;
	
	/**
	 * A container for the rule constructed by {@link #getRule2()}.
	 */
	private LpRule rule2 = null;
	
	/**
	 * The {@link LpParser} instance used in the tests.
	 */
	protected LpParser parser;
	
	/**
	 * The printer used in the tests.
	 */
	protected LpBuffer printer;
	
	/**
	 * A default test case constructor.
	 *
	 * @param testName the name of the test case
	 */
	public LpParserTest(String testName) {
		super(testName);
		parser = new LpParser();
		printer = LpPrettyPrinter.getBuffer();
	}
	
	/**
	 * Tests the parser on null input (a {@link IllegalArgumentException} should
	 * be thrown).
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 */
	public void testSetInputExceptions() {
		String s = null;
		File f = null;
		Reader r = null;
		
		try {
			parser.setInput(s);
			fail("A IllegalArgumentException expected!");
		} catch (IllegalArgumentException e) {
			// behaves as expected
		}
		
		try {
			parser.setInput(f);
			fail("A IllegalArgumentException expected!");
		} catch (IllegalArgumentException e) {
			// behaves as expected
		}
		
		try {
			parser.setInput(r);
			fail("A IllegalArgumentException expected!");
		} catch (IllegalArgumentException e) {
			// behaves as expected
		}
	}
	
	/**
	 * Returns a rule corresponding to the string
	 * <pre>a <- b, not c.</pre>
	 */
	protected LpRule getRule1() {
		if (rule1 == null) {
			LpPredicate predA = LpPredicate.getInstance("a", 0);
			LpPredicate predB = LpPredicate.getInstance("b", 0);
			LpPredicate predC = LpPredicate.getInstance("c", 0);
			LpAtom atomA = LpAtom.getInstance(predA, null);
			LpAtom atomB = LpAtom.getInstance(predB, null);
			LpAtom atomC = LpAtom.getInstance(predC, null);
			LpLiteral litA = atomA.getPositiveLiteral();
			LpLiteral litB = atomB.getPositiveLiteral();
			LpLiteral litNotC = atomC.getNegativeLiteral();
			Set<LpLiteral> body = new LinkedHashSet<LpLiteral>();
			body.add(litB);
			body.add(litNotC);
			rule1 = new LpRule(litA, body);
		}
		assert rule1 != null;
		return rule1;
	}
	
	/**
	 * Returns a rule corresponding to the string
	 * <pre>human(X) :- lives(X), not colour(green, X), not assert(rule(dead(X))).</pre>
	 */
	protected LpRule getRule2() {
		if (rule2 == null) {
			LpPredicate predHuman = LpPredicate.getInstance("human", 1);
			LpPredicate predLives = LpPredicate.getInstance("lives", 1);
			LpPredicate predColour = LpPredicate.getInstance("colour", 2);
			LpPredicate predAssert = LpPredicate.getInstance("assert", 1);
			
			LpFunction funcRule = LpFunction.getInstance("rule", 1);
			LpFunction funcDead = LpFunction.getInstance("dead", 1);
			
			LpVariable varX = LpVariable.getInstance("X");
			
			LpConstant constGreen = LpConstant.getInstance("green");
			
			List<LpTerm> argsHuman = new ArrayList<LpTerm>();
			argsHuman.add(varX);
			LpLiteral litHuman = LpAtom.getInstance(predHuman, argsHuman).getLiteral(true);
			
			List<LpTerm> argsLives = new ArrayList<LpTerm>();
			argsLives.add(varX);
			LpLiteral litLives = LpAtom.getInstance(predLives, argsLives).getLiteral(true);
			
			List<LpTerm> argsColour = new ArrayList<LpTerm>();
			argsColour.add(constGreen);
			argsColour.add(varX);
			LpLiteral litColour = LpAtom.getInstance(predColour, argsColour).getLiteral(false);
			
			List<LpTerm> argsDead = new ArrayList<LpTerm>();
			argsDead.add(varX);
			LpTerm termDead = LpCompoundTerm.getInstance(funcDead, argsDead);
			
			List<LpTerm> argsRule = new ArrayList<LpTerm>();
			argsRule.add(termDead);
			LpTerm termRule = LpCompoundTerm.getInstance(funcRule, argsRule);
			
			List<LpTerm> argsAssert = new ArrayList<LpTerm>();
			argsAssert.add(termRule);
			LpLiteral litAssert = LpAtom.getInstance(predAssert, argsAssert).getLiteral(false);
			
			Set<LpLiteral> body = new LinkedHashSet<LpLiteral>();
			body.add(litLives);
			body.add(litColour);
			body.add(litAssert);
			
			rule2 = new LpRule(litHuman, body);
		}
		assert rule2 != null;
		return rule2;
	}
	
	/**
	 * Tests the parser on an input containing the rule returned by
	 * {@link #getRule1()}.
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 * @throws if a LpParserException is thrown while parsing the input
	 * (should never happen)
	 */
	public void testPropositionalRule() {
		String source = "a <- b, not c.";
		parser.setInput(source);
		assertEquals("A different rule expected!",
				getRule1(), parser.parseRule());
		// no more rules expected...
		assertFalse("No more rules expected!", parser.hasMoreTokens());
		parser.close();
		// should return false after closed
		assertFalse(
				"hasMoreTokens should return false after the parser is closed!",
				parser.hasMoreTokens());
	}
	
	/**
	 * Tests the parser on an input containing the rule returned by
	 * {@link #getRule2()}.
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 * @throws if a LpParserException is thrown while parsing the input
	 * (should never happen)
	 */
	public void testNormalRule() {
		String source = "human(X) :- lives(X), \t\n" +
				"not colour(green, X), not assert(rule(dead(X))).";
		parser.setInput(source);
		assertEquals("A different rule expected!",
				getRule2(), parser.parseRule());
		// no more rules expected...
		assertFalse("No more rules expected!", parser.hasMoreTokens());
		parser.close();
		// should return false after closed
		assertFalse(
				"hasMoreTokens should return false after the parser is closed!",
				parser.hasMoreTokens());
	}
	
	/**
	 * Tests the parser on an input containing the rules returned by
	 * {@link #getRule1()} and {@link #getRule2()}.
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 * @throws if a LpParserException is thrown while parsing the input
	 * (should never happen)
	 */
	public void testBothRules() {
		String source = "a <- b, \t\tnot c." +
				"human(X):- lives(X)\t, \n\r" +
				"not colour(green, X), not assert(rule(dead(X))).";
		parser.setInput(source);
		assertEquals("A different rule expected!",
				getRule1(), parser.parseRule());
		assertEquals("A different rule expected!",
				getRule2(), parser.parseRule());
		// no more rules expected...
		assertFalse("No more rules expected!", parser.hasMoreTokens());
		parser.close();
		// should return false after closed...
		assertFalse(
				"hasMoreTokens should return false after the parser is closed!",
				parser.hasMoreTokens());
		
		// another test on the same source -- closing before all rules are
		// parsed
		parser.setInput(source);
		assertEquals("A different rule expected!",
				getRule1(), parser.parseRule());
		// should have more input
		assertTrue("No more rules expected!", parser.hasMoreTokens());
		// close before the second rule is parsed
		parser.close();
		// should return false after closed...
		assertFalse(
				"hasMoreTokens should return false after the parser is closed!",
				parser.hasMoreTokens());
	}
	
	/**
	 * Parses rules from a file and checks that they are what they should be
	 * (through the {@link LpBuffer} instance)
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 * @throws if a LpParserException is thrown while parsing the input
	 * (should never happen)
	 */
	public void testRulesFromFile() throws IOException {
		doRulesFromFileTest("/data/LpParserTest.dat");
	}
	
	/**
	 * Parses rules from a file and checks that they are what they should be
	 * (through the {@link LpRule#toString()} method)
	 *
	 * @param filename path to the text file with rules
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 * @throws if a LpParserException is thrown while parsing the input
	 * (should never happen)
	 */
	protected void doRulesFromFileTest(String filename) throws IOException {
		Reader reader = new BufferedReader(new InputStreamReader(
				getClass().getResourceAsStream(filename)));
		for (int i = 0; i < 2; i++) {
			// the first part of the file (before the first occurence of '$'
			// contains the parser input
			parser.setInput(new PartReader(reader, '$'));
			LogicProgram rules = parser.parseAllRules();
			
			// the second part contains the expected normal form of rules
			TabSeparatedReader tsr = new TabSeparatedReader(
					new PartReader(reader, '$'));
			for (LpRule r : rules) {
				String expected = tsr.getNext();
				expected = expected.trim();
				assertEquals("A different rule expected!",
						expected, printer.asString(r));
			}
			// check that no more rules are expected
			assertNull("No more rules expected!", tsr.getNext());
		}
		reader.close();
	}
	
	/**
	 * Tests exceptions thrown by {@link LpParser}.
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 */
	public void testExceptions() {
		// trailing dot missing
		doTestException("head <- neck, belly, leg, foot",
				new LpParserException(DOT, "", 1, 31));
		// malformed arrow
		doTestException("\n not a <= b, c, d.",
				new LpParserException(DOT, "<", 2, 8));
		// comma missing (but the exception will be about a missing dot :o)
		doTestException("a <- not a, is_day is_night.",
				new LpParserException(DOT, "is_night", 1, 20));
		// malformed parenthesis (dot will be requested again...)
		doTestException("assert)fact).",
				new LpParserException(DOT, ")", 1, 7));
		// missing right paren
		doTestException("assert(rule(fact).",
				new LpParserException(RIGHT_PAREN, ".", 1, 18));
		// predicate name must begin with a lowercase letter
		doTestException("Fact.",
				new LpParserException(LOWERCASE_WORD, "Fact", 1, 1));
	}
	
	/**
	 * Tests the parser on a ":- ." rule -- a constraint with no literal in its
	 * body. An {@link LpParserException} is expected to be thrown
	 *
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 */
	public void testInvalidRule() {
		LpParserException expected =
				new LpParserException(LOWERCASE_WORD, ".", 1, 4);
		doTestException(":- .", expected);
	}
	
	/**
	 * Checks that an {@link LpParser} instance actually throws the specified
	 * {@link LpParserException} on the given source.
	 *
	 *
	 * @param source the given source
	 * @param expected the exception to be thrown
	 * @throws IOException (wrapped in an {@link lp.util.ExceptionAdapter}) if
	 * an I/O exception occurs while parsing the input (should never happen)
	 */
	private void doTestException(String source, LpParserException expected) {
		parser.setInput(new StringReader(source));
		try {
			parser.parseRule();
			fail("A ParserException expected!");
		} catch (LpParserException e) {
			assertEquals("A different token type expected!",
					expected.getExpected(), e.getExpected());
			assertEquals("A different token type expected!",
					expected.getFound(), e.getFound());
			assertEquals("A different line number expected!",
					expected.getLineNumber(), e.getLineNumber());
			assertEquals("A different position expected!",
					expected.getPosition(), e.getPosition());
		}
		parser.close();
	}
}