package kodkod.test.unit;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.util.HashSet;
import java.util.Set;

import org.junit.Before;
import org.junit.Test;

import kodkod.ast.Decl;
import kodkod.ast.Decls;
import kodkod.ast.Expression;
import kodkod.ast.Formula;
import kodkod.ast.Relation;
import kodkod.ast.Variable;
import kodkod.engine.Evaluator;
import kodkod.instance.Instance;
import kodkod.instance.Tuple;
import kodkod.instance.TupleFactory;
import kodkod.instance.TupleSet;
import kodkod.instance.Universe;

/** 
 * Tests kodkod.engine.Evaluator using the instance produced by running
 * the AlloyAnalyzer on examples/puzzles/handshake.als
 *
 * @author Emina Torlak 
 */
public class EvaluatorTest {

	private Evaluator evaluator;
	private final Relation univ, hilary, jocelyn, person, spouse, shaken;

	/**
	 * Constructor for EvaluatorTest.
	 * @param arg0
	 */
	public EvaluatorTest() {
		univ = Relation.unary("univ");
		hilary = Relation.unary("hilary");
		jocelyn = Relation.unary("jocelyn");
		person = Relation.unary("person");
		spouse = Relation.binary("spouse");
		shaken = Relation.binary("shaken");
	}


	@Before
	public void setUp() throws Exception {
		final Universe u = 
				new Universe("Jocelyn_0", "Hilary_0",  "Person_0", "Person_1", "Person_2", 
						"Person_3",  "Person_4", "Person_5", "Person_6",  "Person_7");
		final TupleFactory f = u.factory();
		final Instance inst = new Instance(u);
		inst.add(univ, f.allOf(1));
		inst.add(hilary, f.setOf("Hilary_0"));
		inst.add(jocelyn, f.setOf("Jocelyn_0"));
		inst.add(person, f.allOf(1));
		inst.add(spouse, f.setOf(f.tuple("Jocelyn_0", "Hilary_0"),
								 f.tuple("Hilary_0", "Jocelyn_0"),
								 f.tuple("Person_0", "Person_1"),
								 f.tuple("Person_1", "Person_0"),
								 f.tuple("Person_2", "Person_3"),
								 f.tuple("Person_3", "Person_2"),
								 f.tuple("Person_4", "Person_5"),
								 f.tuple("Person_5", "Person_4"),
								 f.tuple("Person_6", "Person_7"),
								 f.tuple("Person_7", "Person_6")));
		inst.add(shaken, f.setOf(f.tuple("Jocelyn_0", "Person_1"),
								 f.tuple("Jocelyn_0", "Person_3"),
								 f.tuple("Jocelyn_0", "Person_4"),
								 f.tuple("Jocelyn_0", "Person_7"),
								 f.tuple("Hilary_0", "Person_1"),
								 f.tuple("Hilary_0", "Person_3"),
								 f.tuple("Hilary_0", "Person_4"),
								 f.tuple("Hilary_0", "Person_7"),
								 f.tuple("Person_0", "Person_3"),
								 f.tuple("Person_0", "Person_4"),
								 f.tuple("Person_0", "Person_7"),
								 f.tuple("Person_1", "Jocelyn_0"),
								 f.tuple("Person_1", "Hilary_0"),
								 f.tuple("Person_1", "Person_3"),
								 f.tuple("Person_1", "Person_4"),
								 f.tuple("Person_1", "Person_7"),
								 f.tuple("Person_3", "Jocelyn_0"),
								 f.tuple("Person_3", "Hilary_0"),
								 f.tuple("Person_3", "Person_0"),
								 f.tuple("Person_3", "Person_1"),
								 f.tuple("Person_3", "Person_4"),
								 f.tuple("Person_3", "Person_5"),
								 f.tuple("Person_3", "Person_6"),
								 f.tuple("Person_3", "Person_7"),
								 f.tuple("Person_4", "Jocelyn_0"),
								 f.tuple("Person_4", "Hilary_0"),
								 f.tuple("Person_4", "Person_0"),
								 f.tuple("Person_4", "Person_1"),
								 f.tuple("Person_4", "Person_3"),
								 f.tuple("Person_4", "Person_7"),
								 f.tuple("Person_5", "Person_3"),
								 f.tuple("Person_5", "Person_7"),
								 f.tuple("Person_6", "Person_3"),
								 f.tuple("Person_7", "Jocelyn_0"),
								 f.tuple("Person_7", "Hilary_0"),
								 f.tuple("Person_7", "Person_0"),
								 f.tuple("Person_7", "Person_1"),
								 f.tuple("Person_7", "Person_3"),
								 f.tuple("Person_7", "Person_4"),
								 f.tuple("Person_7", "Person_5")));
		evaluator = new Evaluator(inst);
	}


	private boolean eval(Formula formula) {
		return evaluator.evaluate(formula);
	}

	private Set<Tuple> eval(Expression expression) {
		return evaluator.evaluate(expression);
	}

	private Set<Tuple> value(Relation relation) {
		return evaluator.instance().tuples(relation);
	}


	@Test
	public final void testEvalUnion() {
		// Hilary + Hilary = Hilary
		assertEquals(eval(hilary.union(hilary)), value(hilary));
		// Hilary + Jocelyn + Person = Person
		assertEquals(eval(hilary.union(jocelyn).union(person)), value(person));
		// spouse + shaken = spouse + shaken
		Set<Tuple> spousePlusShaken = new HashSet<Tuple>();
		spousePlusShaken.addAll(value(spouse));
		spousePlusShaken.addAll(value(shaken));
		assertEquals(eval(spouse.union(shaken)), spousePlusShaken);
		// shaken + spouse = spouse + shaken
		assertEquals(eval(shaken.union(spouse)), spousePlusShaken);
		// spouse + Person = arity mismatch
		try {
			eval(spouse.union(person));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}

	}

	@Test
	public final void testEvalDifference() {
		// Jocelyn - Jocelyn = {}
		assertTrue(eval(jocelyn.difference(jocelyn)).isEmpty());
		// Hilary - Jocelyn = Hilary
		assertEquals(value(hilary), eval(hilary.difference(jocelyn)));
		// spouse + shaken - spouse = shaken
		assertEquals(value(shaken), eval(spouse.union(shaken).difference(spouse)));
		// spouse - Person = arity mismatch
		try {
			eval(spouse.difference(person));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}

	}

	@Test
	public final void testEvalJoin() {
		// Hilary.spouse = Jocelyn
		assertEquals(eval(hilary.join(spouse)), value(jocelyn));
		// arity(spouse.shaken) = 2
		assertEquals(spouse.join(shaken).arity(), 2);
		// spouse.Person = univ
		assertEquals(eval(spouse.join(person)), value(univ));
		// spouse.spouse.Hilary = Hilary
		assertEquals(eval(spouse.join(spouse).join(hilary)), value(hilary));
		// (univ - Person.shaken).shaken = {}
		assertTrue(eval(univ.difference(person.join(shaken)).join(shaken)).isEmpty());
		// Hilary.Jocelyn = arity mismatch
		try {
			eval(hilary.join(jocelyn));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}
	}


	@Test
	public final void testEvalProduct() {
		// Hilary->Jocelyn = Hilary->Jocelyn
		final Set<Tuple> hilaryAndJocelyn = eval(hilary.product(jocelyn));
		final Tuple hj = hilaryAndJocelyn.iterator().next();
		assertEquals(hilaryAndJocelyn.size(), 1);
		assertEquals(hj.atom(0), value(hilary).iterator().next().atom(0));
		assertEquals(hj.atom(1), value(jocelyn).iterator().next().atom(0));

		// Person->(spouse->shaken) = (Person->spouse)->shaken
		assertEquals(eval(person.product(spouse.product(shaken))),
				eval(person.product(spouse).product(shaken)));
		// Person->(spouse + shaken) = Person->spouse + Person->shaken
		assertEquals(eval(person.product(spouse.union(shaken))),
				eval(person.product(spouse).union(person.product(shaken))));
		// arity(spouse->shaken) = 4
		assertEquals(spouse.product(shaken).arity(), 4);
	}

	@Test
	public final void testEvalIntersection() {
		// Hilary & Person = Hilary
		assertEquals(eval(hilary.intersection(person)), value(hilary));
		// Hilary & Person = Person & Hilary
		assertEquals(eval(hilary.intersection(person)), eval(person.intersection(hilary)));
		// spouse & shaken = {}
		assertTrue(eval(spouse.intersection(shaken)).isEmpty());   
		// spouse & Person = arity mismatch
		try {
			eval(spouse.intersection(person));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}

	}

	@Test
	public final void testEvalOverride() {
		// Hilary ++ Hilary = Hilary
		assertEquals(eval(hilary.override(hilary)), value(hilary));
		// Hilary ++ Jocelyn = Hilary + Jocelyn
		assertEquals(eval(hilary.override(jocelyn)), eval(hilary.union(jocelyn)));
		// spouse ++ shaken = shaken + (spouse - (shaken.Person)->Person)
		assertEquals(eval(spouse.override(shaken)), 
				eval(shaken.union(spouse.difference(shaken.join(person).product(person)))));
	}

	@Test
	public final void testEvalTranspose() {
		// ~spouse = spouse
		assertEquals(eval(spouse.transpose()), value(spouse));
		// ~(Hilary->Jocelyn) = Jocelyn->Hilary
		assertEquals(eval(hilary.product(jocelyn).transpose()), eval(jocelyn.product(hilary)));
		// ~(~shaken) = shaken
		assertEquals(eval(shaken.transpose().transpose()), value(shaken));
		// ~Person = arity error
		try {
			eval(person.transpose());
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}

	}

	@Test
	public final void testEvalTransitiveClosure() {

		//  ^r = value(^r)
		final Relation r = Relation.binary("r");
		final Universe u = evaluator.instance().universe();
		final TupleFactory f = u.factory();
		final Instance instance = new Instance(u);

		// value(r) = u[0]->u[1] + u[1]->u[2] + u[2]->u[3] + u[3]->u[4] 
		TupleSet s = f.noneOf(r.arity());
		for (int i = 0; i < 4; i++) s.add(f.tuple(u.atom(i), u.atom(i+1)));
		instance.add(r,s);
		// value(^r) = value(r) + u[0]->u[2] + u[0]->u[3] + u[0]->u[4] + u[1]->u[3] u[1]->u[4] + u[2]->u[4]
		Set<Tuple> result = new HashSet<Tuple>();
		for (int i = 0; i  < 4; i++) {
			for (int j = i+1; j < 5; j++) {
				result.add(f.tuple(u.atom(i), u.atom(j)));
			}
		}
		assertEquals((new Evaluator(instance)).evaluate(r.closure()), result);
		// value(*r) = value(^r) + iden
		for(int i = 0; i < 10; i++) {
			result.add(f.tuple(u.atom(i), u.atom(i)));
		}

		assertEquals((new Evaluator(instance)).evaluate(r.reflexiveClosure()), result);
	}

	@Test
	public final void testEvalSubset() {
		// Hilary in Person = true
		assertTrue(eval(hilary.in(person)));
		// shaken in spouse = false
		assertFalse(eval(shaken.in(spouse)));
		// spouse in Person->Person = true
		assertTrue(eval(spouse.in(person.product(person))));
		// spouse in Person = arity mismatch
		try {
			eval(spouse.in(person));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}
	}


	@Test
	public final void testEvalEquals() {
		// Person = univ    = true
		assertTrue(eval(person.eq(univ)));
		// univ = Person    = true
		assertTrue(eval(univ.eq(person)));
		// spouse = shaken  = false
		assertFalse(eval(spouse.eq(shaken)));
		// shaken = Person
		try {
			eval(shaken.in(person));
			fail("Expected IllegalArgumentException");
		} catch (IllegalArgumentException iae) {}
	}

	@Test
	public final void testEvalAnd() {
		// Hilary in Person && Jocelyn in Person  = true
		assertTrue(eval(hilary.in(person).and(jocelyn.in(person))));
		// Jocelyn in Person && Hilary in Person  = true
		assertTrue(eval(jocelyn.in(person).and(hilary.in(person))));
		// shaken in spouse && univ = Person      = false
		assertFalse(eval(shaken.in(spouse).and(univ.eq(person))));
		// Person = univ && spouse in shaken      = false
		assertFalse(eval(person.eq(univ).and(spouse.in(shaken))));
		// spouse in shaken && Hilary = Jocelyn   = false
		assertFalse(eval(spouse.in(shaken).and(hilary.eq(jocelyn))));
	}

	@Test
	public final void testEvalOr() {
		// Hilary in Person || Jocelyn in Person  = true
		assertTrue(eval(hilary.in(person).or(jocelyn.in(person))));
		// Jocelyn in Person || Hilary in Person  = true
		assertTrue(eval(jocelyn.in(person).or(hilary.in(person))));
		// shaken in spouse || univ = Person      = true
		assertTrue(eval(shaken.in(spouse).or(univ.eq(person))));
		// Person = univ || spouse in shaken      = true
		assertTrue(eval(person.eq(univ).or(spouse.in(shaken))));
		// spouse in shaken || Hilary = Jocelyn   = false
		assertFalse(eval(spouse.in(shaken).or(hilary.eq(jocelyn))));
	}

	@Test
	public final void testEvalNot() {
		// !(Hilary in Person) = false
		assertFalse(eval(hilary.in(person).not()));
		// !(Hilary = Jocelyn) = true
		assertTrue(eval(hilary.eq(jocelyn).not()));
	}

	@Test
	public final void testEvalImplies() {
		// Hilary in Person => Jocelyn in Person  = true
		assertTrue(eval(hilary.in(person).implies(jocelyn.in(person))));
		// Hilary in Person => Person in Jocelyn  = false
		assertFalse(eval(hilary.in(person).implies(person.in(jocelyn))));
		// Hilary = Jocelyn => Person = univ      = true
		assertTrue(eval(hilary.eq(jocelyn).implies(person.eq(univ))));
		// Hilary = Jocelyn => spouse = shaken    = true
		assertTrue(eval(hilary.eq(jocelyn).implies(spouse.eq(shaken))));
	}

	@Test
	public final void testEvalIff() {
		// Hilary in Person <=> Jocelyn in Person  = true
		assertTrue(eval(hilary.in(person).iff(jocelyn.in(person))));
		// Hilary = Jocelyn <=> spouse = shaken    = true
		assertTrue(eval(hilary.eq(jocelyn).iff(spouse.eq(shaken))));
		// shaken in spouse <=> univ = Person      = false
		assertFalse(eval(shaken.in(spouse).iff(univ.eq(person))));
		// Person = univ <=> spouse in shaken      = false
		assertFalse(eval(person.eq(univ).iff(spouse.in(shaken))));   
	}

	@Test
	public final void testMultiplicityFormula() {
		// some Person            = true
		assertTrue(eval(person.some()));
		// some (Person - univ)   = false
		assertFalse(eval(person.difference(univ).some()));
		// no Person            = false
		assertFalse(eval(person.no()));
		// no (Person - univ)   = true
		assertTrue(eval(person.difference(univ).no()));
		// one Hilary           = true
		assertTrue(eval(hilary.one()));
		// one spouse           = false
		assertFalse(eval(spouse.one()));
		// lone (Person - univ) = true
		assertTrue(eval(person.difference(univ).lone()));
		// lone shaken          = false
		assertFalse(eval(shaken.lone()));

	}

	@Test
	public final void testQuantifiedFormula() {

		final Variable p = Variable.unary("p"), q = Variable.unary("q");
		final Decl pdecl = p.oneOf(person), qdecl = q.oneOf(person);
		final Decls pqdecls = pdecl.and(qdecl);
		// all p: Person | some p.spouse                            = true
		assertTrue(eval(p.join(spouse).some().forAll(pdecl)));
		// all p, q: Person | (p.spouse = q) => ! (q in p.shaken)   = true
		assertTrue(eval((p.join(spouse).eq(q).implies(q.in(p.join(shaken)).not()).forAll(pqdecls))));
		// some p: Person | no p.shaken                             = true
		assertTrue(eval(p.join(shaken).no().forSome(pdecl)));
		// all p: Person | some q: Person | p.shaken = q            = false
		assertFalse(eval((p.join(shaken).eq(q).forSome(qdecl)).forAll(pdecl)));
		// some p, q: Person | !(p = q) && (p.shaken = q.shaken)    = true
		assertTrue(eval(p.eq(q).not().and(p.join(shaken).eq(q.join(shaken))).forSome(pqdecls)));
		// some p: Person | all q: Person-p | p in q.shaken         = false
		assertFalse(eval((p.in(q.join(shaken)).forAll(q.oneOf(person.difference(p)))).forSome(pdecl)));
	}

	@Test
	public final void testComprehension() {
		final Variable[] vars = new Variable[3];
		final Decl[] decls = new Decl[3];
		for (int i = 0; i < 3; i++) {
			Variable v = Variable.unary("v"+i);
			Decl d = v.oneOf(person);
			vars[i] = v;
			decls[i] = d;
		}

		// {v0: Person | no v0.shaken} = univ - shaken.Person
		assertEquals(eval(vars[0].join(shaken).no().comprehension(decls[0])), eval(univ.difference(shaken.join(person))));
		// {v0, v1: Person | v1 in v0.shaken} = shaken
		assertEquals(eval(vars[1].in(vars[0].join(shaken)).comprehension(decls[0].and(decls[1]))),
				value(shaken));
		// {v0, v1, v2: Person | no v1.shaken} = Person->(univ - shaken.Person)->Person
		assertEquals(eval(vars[1].join(shaken).no().comprehension(decls[0].and(decls[1]).and(decls[2]))),
				eval(person.product(univ.difference(shaken.join(person))).product(person)));
	}


}