package jbse.dec;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.hamcrest.CoreMatchers.instanceOf;

import java.util.ArrayList;
import java.util.TreeSet;

import jbse.common.Type;
import jbse.common.exc.InvalidInputException;
import jbse.dec.exc.DecisionException;
import jbse.mem.ClauseAssume;
import jbse.rewr.CalculatorRewriting;
import jbse.rewr.RewriterOperationOnSimplex;
import jbse.tree.DecisionAlternative_XCMPY_Eq;
import jbse.tree.DecisionAlternative_XCMPY_Gt;
import jbse.tree.DecisionAlternative_XCMPY_Lt;
import jbse.tree.DecisionAlternative_IFX;
import jbse.tree.DecisionAlternative_XCMPY;
import jbse.tree.DecisionAlternativeComparators;
import jbse.tree.DecisionAlternative_IFX_False;
import jbse.tree.DecisionAlternative_IFX_True;
import jbse.val.Expression;
import jbse.val.Primitive;
import jbse.val.Simplex;
import jbse.val.Term;
import jbse.val.exc.InvalidOperandException;
import jbse.val.exc.InvalidTypeException;

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

public class DecisionProcedureTest {
	private static final String SWITCH_CHAR = System.getProperty("os.name").toLowerCase().contains("windows") ? "/" : "-";
    private static final ArrayList<String> Z3_COMMAND_LINE = new ArrayList<>();
    
    static {
    	Z3_COMMAND_LINE.add("/opt/local/bin/z3");
    	Z3_COMMAND_LINE.add(SWITCH_CHAR + "smt2");
    	Z3_COMMAND_LINE.add(SWITCH_CHAR + "in");
    	Z3_COMMAND_LINE.add(SWITCH_CHAR + "t:10");
    }
    
    CalculatorRewriting calc;
    DecisionAlternativeComparators cmp;
    DecisionProcedureAlgorithms dec;

    @Before
    public void setUp() throws DecisionException, InvalidInputException {
        this.calc = new CalculatorRewriting();
        this.calc.addRewriter(new RewriterOperationOnSimplex());
        this.cmp = new DecisionAlternativeComparators();
        this.dec = 
        new DecisionProcedureAlgorithms(
          new DecisionProcedureSMTLIB2_AUFNIRA(
            new DecisionProcedureAlwSat(this.calc), Z3_COMMAND_LINE));
    }

    @Test
    public void testSimplifyDecision1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // 2 > 4
        Primitive p = this.calc.pushInt(2).gt(this.calc.valInt(4)).pop();
        TreeSet<DecisionAlternative_IFX> d = new TreeSet<>(this.cmp.get(DecisionAlternative_IFX.class));
        this.dec.decide_IFX(p, d);

        //expected: {F_concrete}
        assertEquals(1, d.size());
        DecisionAlternative_IFX dai = d.first();
        assertTrue(dai.concrete());
        assertThat(dai, instanceOf(DecisionAlternative_IFX_False.class));
    }

    @Test
    public void testSimplifyDecision2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // true |- (A > 0) && (A <= 1)
        Term A = this.calc.valTerm(Type.INT, "A");
        Expression e = (Expression) this.calc.push(A).gt(this.calc.valInt(0)).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).le(this.calc.valInt(1)).pop()).pop();
        TreeSet<DecisionAlternative_IFX> d = new TreeSet<>(this.cmp.get(DecisionAlternative_IFX.class));
        this.dec.decide_IFX(e, d);

        //expected: {T_nonconcrete, F_nonconcrete}
        assertEquals(2, d.size());
        DecisionAlternative_IFX dai1 = d.first();
        d.remove(dai1);
        DecisionAlternative_IFX dai2 = d.first();
        assertFalse(dai1.concrete());
        assertFalse(dai2.concrete());
        assertThat(dai1, instanceOf(DecisionAlternative_IFX_True.class));
        assertThat(dai2, instanceOf(DecisionAlternative_IFX_False.class));
    }

    @Test
    public void testSimplifyComparison1() 
    throws InvalidInputException, DecisionException {
        // true |- 2 ? 5
        Simplex two = this.calc.valInt(2);
        Simplex five = this.calc.valInt(5);
        TreeSet<DecisionAlternative_XCMPY> d = new TreeSet<>(this.cmp.get(DecisionAlternative_XCMPY.class));
        this.dec.decide_XCMPY(two, five, d);

        //expected {LT_concrete}
        assertEquals(1, d.size());
        DecisionAlternative_XCMPY dac = d.first();
        assertTrue(dac.concrete());
        assertThat(dac, instanceOf(DecisionAlternative_XCMPY_Lt.class));
    }

    @Test
    public void testSimplifyComparison2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // A >= 0 |- 2 * A ? A
        Term A = this.calc.valTerm(Type.INT, "A");
        Expression Agezero = (Expression) this.calc.push(A).ge(this.calc.valInt(0)).pop();
        Expression Atwice = (Expression) this.calc.push(A).mul(this.calc.valInt(2)).pop();
        TreeSet<DecisionAlternative_XCMPY> d = new TreeSet<>(this.cmp.get(DecisionAlternative_XCMPY.class));
        this.dec.pushAssumption(new ClauseAssume(Agezero));
        this.dec.decide_XCMPY(Atwice, A, d);

        //expected {GT_nonconcrete, EQ_nonconcrete}
        assertEquals(2, d.size());
        DecisionAlternative_XCMPY dac1 = d.first();
        d.remove(dac1);
        DecisionAlternative_XCMPY dac2 = d.first();
        assertFalse(dac1.concrete());
        assertFalse(dac2.concrete());
        assertThat(dac1, instanceOf(DecisionAlternative_XCMPY_Gt.class));
        assertThat(dac2, instanceOf(DecisionAlternative_XCMPY_Eq.class));
    }

    @Test
    public void testSimplify1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // true |- (A >= 0) && (A < 5) && !(A == 1) && !(A == 2) && !(A == 3)
        Term A = this.calc.valTerm(Type.INT, "A");
        Expression e = (Expression) this.calc.push(A).ge(this.calc.valInt(0)).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).lt(this.calc.valInt(5)).pop()).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).eq(this.calc.valInt(1)).not().pop()).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).eq(this.calc.valInt(2)).not().pop()).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).eq(this.calc.valInt(3)).not().pop()).pop();

        //expected satisfiable (by A == 4)
        assertTrue(this.dec.isSat(e));
    }

    @Test
    public void testSimplify2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // true |- ((A >= 0) || (A < -3)) && (A == -1)  
        Term A = this.calc.valTerm(Type.INT, "A");
        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(A).ge(this.calc.valInt(0)).pop()).pop();
        e = (Expression) this.calc.push(e).or(this.calc.push(A).lt(this.calc.valInt(-3)).pop()).pop();
        e = (Expression) this.calc.push(e).and(this.calc.push(A).eq(this.calc.valInt(-1)).pop()).pop();

        //expected unsatisfiable
        assertFalse(this.dec.isSat((Expression) e));
    }

    @Test
    public void testSimplify3() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // true |- (X && !Y) || (!X && Y)

        //boolean terms are emulated with satisfiable independent clauses
        Term A = this.calc.valTerm(Type.INT, "A");
        Term B = this.calc.valTerm(Type.INT, "B");
        Primitive X = this.calc.push(A).ge(this.calc.valInt(0)).pop();
        Primitive Y = this.calc.push(B).ge(this.calc.valInt(0)).pop();
        Primitive e0 = this.calc.valBoolean(true);
        e0 = this.calc.push(e0).and(X).pop();
        e0 = this.calc.push(e0).and(this.calc.push(Y).not().pop()).pop();
        Primitive e1 = this.calc.valBoolean(true);
        e1 = this.calc.push(e1).and(this.calc.push(X).not().pop()).pop();
        e1 = this.calc.push(e1).and(Y).pop();
        Primitive e = this.calc.push(e0).or(e1).pop();

        //expected satisfiable
        assertTrue(this.dec.isSat((Expression) e));
    }

    @Test
    public void testAssumption1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        //assume:
        //(0 > {V12}) && 
        //     ({V14} >= 0) && 
        //     (0 < {V14}) && 
        //     (0 <= {V15}) && 
        //     ({V20} >= 0) && 
        //     (0 < {V20})) && 
        //     ({V12} != -2147483648) && 
        //(- {V12} <= {V15}) && 
        //(- {V12} < 65536) && 
        //(- {V12} - ({X0} * 8.0 + {X0} * 2.0)) == 0)
        Term V12 = this.calc.valTerm(Type.INT, "V12");
        //Term V14 = this.calc.valTerm(Type.INT, "V14");
        Term V15 = this.calc.valTerm(Type.INT, "V15");
        //Term V20 = this.calc.valTerm(Type.INT, "V20");
        Term X0 = this.calc.valTerm(Type.INT, "X0");
        Simplex zero = this.calc.valInt(0);
        //Simplex bottom = this.calc.valInt(-2147483648);
        Simplex top = this.calc.valInt(65536);
        Simplex eight = this.calc.valInt(8);
        Simplex two = this.calc.valInt(2);

        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(zero).gt(V12).pop()).pop();
        //e = e.and(V14.ge(zero));
        //e = e.and(zero.lt(V14));
        //e = e.and(zero.le(V15));
        //e = e.and(V20.ge(zero));
        //e = e.and(zero.lt(V20));
        //e = e.and(V12.ne(bottom));
        e = this.calc.push(e).and(this.calc.push(V12).neg().le(V15).pop()).pop();
        e = this.calc.push(e).and(this.calc.push(V12).neg().lt(top).pop()).pop();
        e = this.calc.push(e).and(this.calc.push(V12).neg().sub(this.calc.push(X0).mul(eight).add(this.calc.push(X0).mul(two).pop()).pop()).eq(zero).pop()).pop();

        //this.dec.pushClause(e);

        //Expression ee = (Expression) X0.ne(zero);

        assertTrue(this.dec.isSat((Expression) e));
    }

    @Test
    public void testAssumption2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        //assume:
        //0 > B &&
        //-B <= C &&
        //B + 10.0 * A = 0 
        Term A = this.calc.valTerm(Type.INT, "A");
        Term B = this.calc.valTerm(Type.INT, "B");
        Term C = this.calc.valTerm(Type.INT, "C");
        Simplex zero = this.calc.valInt(0);
        Simplex ten = this.calc.valInt(10);

        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(zero).gt(B).pop()).pop();
        e = this.calc.push(e).and(this.calc.push(B).neg().le(C).pop()).pop();
        e = this.calc.push(e).and(this.calc.push(B).add(this.calc.push(A).mul(ten).pop()).eq(zero).pop()).pop();

        assertTrue(this.dec.isSat((Expression) e));
        //shows a past bug: the Sicstus server first simplifies the expression with the clpqr solver, 
        //which simplifies the third constraint as B - 1/10*C <= 0 , then reuses the simplified constraint
        //to feed the integer solver. The latter apparently solves 1/10 as 0, yielding an unsatisfiable set 
        //of constraints.
    }

    //Boundary value for integers (regression bug of the Sicstus server)
    @Test
    public void testBoundary1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        Expression e = (Expression) this.calc.pushTerm(Type.INT, "A").eq(this.calc.valInt(Integer.MIN_VALUE)).pop();
        assertTrue(this.dec.isSat(e));
    }

    //Other boundary value for integers
    @Test
    public void testBoundary2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        Expression e = (Expression) this.calc.pushTerm(Type.INT, "A").eq(this.calc.valInt(Integer.MAX_VALUE)).pop();
        assertTrue(this.dec.isSat(e));
    }

    //Test floats
    @Test
    public void testType1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        Expression e = (Expression) this.calc.pushTerm(Type.FLOAT, "A").gt(this.calc.valInt(0)).and(this.calc.pushTerm(Type.FLOAT, "A").lt(this.calc.valInt(1)).pop()).pop();
        assertTrue(this.dec.isSat(e));
    }

    //Test ints
    @Test
    public void testType2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        Expression e = (Expression) this.calc.pushTerm(Type.INT, "A").gt(this.calc.valInt(0)).and(this.calc.pushTerm(Type.FLOAT, "A").lt(this.calc.valInt(1)).pop()).pop();
        assertFalse(this.dec.isSat(e));
    }

    //Test integer division (Sicstus bug)
    @Test
    public void testIDiv() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        // 0 <= A < B && A >= B / 2 && B = 1 (for integer division A == 0 is a solution, no solution for real division).
        final Term A = this.calc.valTerm(Type.INT, "A");
        final Term B = this.calc.valTerm(Type.INT, "B");
        final Expression e = (Expression) this.calc.push(A).ge(this.calc.valInt(0)).and(this.calc.push(A).lt(B).pop()).and(this.calc.push(A).ge(this.calc.push(B).div(this.calc.valInt(2)).pop()).pop()).and(this.calc.push(B).eq(this.calc.valInt(1)).pop()).pop();
        assertTrue(this.dec.isSat(e));
    }

    //Old Sicstus bug
    @Test
    public void testOther1() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        //true |- A/B + (C - C) / D < E  
        Term A = this.calc.valTerm(Type.INT, "A");
        Term B = this.calc.valTerm(Type.INT, "B");
        Term C = this.calc.valTerm(Type.INT, "C");
        Term D = this.calc.valTerm(Type.INT, "D");
        Term E = this.calc.valTerm(Type.INT, "E");

        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(A).div(B).add(this.calc.push(C).sub(C).div(D).pop()).lt(E).pop()).pop();

        //expected satisfiable
        assertTrue(this.dec.isSat((Expression) e));
    }

    @Test
    public void testOther2() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        //true |- 2 - (3 + A) <= A
        Simplex two = this.calc.valInt(2);
        Simplex three = this.calc.valInt(3);
        Term A = this.calc.valTerm(Type.INT, "A");

        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(two).sub(this.calc.push(three).add(A).pop()).le(A).pop()).pop();

        //expected satisfiable
        assertTrue(this.dec.isSat((Expression) e));
    }

    @Test
    public void testOther3() 
    throws InvalidInputException, DecisionException, InvalidOperandException, InvalidTypeException {
        Simplex two = this.calc.valInt(2);
        Term A = this.calc.valTerm(Type.INT, "A");

        Primitive e = this.calc.valBoolean(true);
        e = this.calc.push(e).and(this.calc.push(A).div(two).lt(two).pop()).pop();

        //expected satisfiable
        assertTrue(this.dec.isSat((Expression) e));
    }	
}