package org.mvel2.tests.core;

import org.mvel2.MVEL;
import org.mvel2.ParserConfiguration;
import org.mvel2.ParserContext;
import org.mvel2.compiler.CompiledExpression;
import org.mvel2.compiler.ExecutableStatement;
import org.mvel2.compiler.ExpressionCompiler;
import org.mvel2.integration.ResolverTools;
import org.mvel2.integration.impl.ClassImportResolverFactory;
import org.mvel2.integration.impl.MapVariableResolverFactory;
import org.mvel2.optimizers.OptimizerFactory;
import org.mvel2.tests.core.res.Bar;
import org.mvel2.tests.core.res.Base;
import org.mvel2.tests.core.res.Foo;
import org.mvel2.tests.core.res.MyEnum;
import org.mvel2.tests.core.res.Thing;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.mvel2.MVEL.compileExpression;
import static org.mvel2.MVEL.executeExpression;

public class WithTests extends AbstractTest {
  public void testWith() {
    String ex = "with (foo) {aValue = 'One',bValue='Two'}; foo.aValue + foo.bValue;";

    Map vars = createTestMap();
    Object o = MVEL.eval(ex, vars);
    assertEquals("OneTwo", o);

    assertEquals("OneTwo", test("with (foo) {aValue = 'One',bValue='Two'}; foo.aValue + foo.bValue;"));
  }

  public void testWith2() {
    assertEquals("OneTwoOne", test(
        "var y; with (foo) { \n" +
            "aValue = (y = 'One'), // this is a comment \n" +
            "bValue='Two'  // this is also a comment \n" +
            "}; \n" +
            "foo.aValue + foo.bValue + y;"));
  }

  public void testWith3() {
    String ex = "with (foo) {aValue = 'One',bValue='Two'}; with (foo) {aValue += 'One', bValue += 'Two'}; foo.aValue + foo.bValue;";

    Map vars = createTestMap();

    assertEquals("OneOneTwoTwo", MVEL.eval(ex, vars));


    assertEquals("OneOneTwoTwo", test("with (foo) {aValue = 'One',bValue='Two'}; with (foo) {aValue += 'One', bValue += 'Two'}; foo.aValue + foo.bValue;"));
  }


  public void testWith4() {
    assertEquals(10, test("with (foo) {countTest += 5 }; with (foo) { countTest *= 2 }; foo.countTest"));
  }

  public void testWith5() {
    String expr = "with (foo) { countTest += 5, \n" +
        "// foobar!\n" +
        "aValue = 'Hello',\n" +
        "/** Comment! **/\n" +
        "bValue = 'Goodbye'\n }; with (foo) { countTest *= 2 }; foo";

    Map vars = createTestMap();

    assertEquals(true, MVEL.eval(expr, vars) instanceof Foo);

    Foo foo = (Foo) test(expr);

    assertEquals(10, foo.getCountTest());
    assertEquals("Hello", foo.aValue);
    assertEquals("Goodbye", foo.bValue);
  }

  public void testInlineWith() {
    CompiledExpression expr = new ExpressionCompiler("foo.{name='poopy', aValue='bar'}").compile();
    Foo f = (Foo) executeExpression(expr, createTestMap());
    assertEquals("poopy", f.getName());
    assertEquals("bar", f.aValue);
  }

  public void testInlineWith2() {
    CompiledExpression expr = new ExpressionCompiler("foo.{name = 'poopy', aValue = 'bar', bar.{name = 'foobie'}}").compile();

    Foo f = (Foo) executeExpression(expr, createTestMap());

    assertEquals("poopy", f.getName());
    assertEquals("bar", f.aValue);
    assertEquals("foobie", f.getBar().getName());
  }

  public void testInlineWith3() {
    CompiledExpression expr = new ExpressionCompiler("foo.{name = 'poopy', aValue = 'bar', bar.{name = 'foobie'}, toUC('doopy')}").compile();

    Foo f = (Foo) executeExpression(expr, createTestMap());

    assertEquals("poopy", f.getName());
    assertEquals("bar", f.aValue);
    assertEquals("foobie", f.getBar().getName());
    assertEquals("doopy", f.register);
  }

  public void testInlineWith3a() {
    CompiledExpression expr = new ExpressionCompiler("foo.{name='poopy',aValue='bar',bar.{name='foobie'},toUC('doopy')}").compile();

    Foo f = (Foo) executeExpression(expr, createTestMap());

    assertEquals("poopy", f.getName());
    assertEquals("bar", f.aValue);
    assertEquals("foobie", f.getBar().getName());
    assertEquals("doopy", f.register);
  }

  public void testInlineWith4() {
    OptimizerFactory.setDefaultOptimizer("ASM");
    ParserContext pCtx = new ParserContext();
    pCtx.addImport(Foo.class);

    ExpressionCompiler expr = new ExpressionCompiler("new Foo().{ name = 'bar' }", pCtx);
    CompiledExpression c = expr.compile();

    Foo f = (Foo) executeExpression(c);

    assertEquals("bar", f.getName());

    f = (Foo) executeExpression(c);

    assertEquals("bar", f.getName());
  }

  public void testInlineWith5() {
    OptimizerFactory.setDefaultOptimizer("ASM");

    ParserContext pCtx = new ParserContext();
    pCtx.setStrongTyping(true);

    pCtx.addInput("foo", Foo.class);

    CompiledExpression expr = new ExpressionCompiler("foo.{name='poopy', aValue='bar'}", pCtx).compile();
    Foo f = (Foo) executeExpression(expr, createTestMap());
    assertEquals("poopy", f.getName());
    assertEquals("bar", f.aValue);
  }

  public void testInlineWithImpliedThis() {
    Base b = new Base();
    ExpressionCompiler expr = new ExpressionCompiler(".{ data = 'foo' }");
    CompiledExpression compiled = expr.compile();

    executeExpression(compiled, b);

    assertEquals(b.data, "foo");
  }

  public void testSingleMethodCall() {
    Base b = new Base();

    Map map = new HashMap();
    map.put("base", b);

    MVEL.eval("base.{ populate() }", map);

    assertEquals("sarah", b.barfoo);
  }

  public void testWithMultipleMethodCalls() {
    ParserContext ctx = ParserContext.create().stronglyTyped().withInput("foo", Foo.class);

    MVEL.compileExpression("with (foo) { setName('foo'), setBar(null) }", ctx);
  }

  public void testNewUsingWith() {
    ParserContext ctx = new ParserContext();
    ctx.setStrongTyping(true);
    ctx.addImport(Foo.class);
    ctx.addImport(Bar.class);

    Serializable s = compileExpression("[ 'foo' : (with ( new Foo() )" +
        " { bar = with ( new Bar() ) { name = 'ziggy' } }) ]",
        ctx);

    OptimizerFactory.setDefaultOptimizer("reflective");
    assertEquals("ziggy",
        (((Foo) ((Map<?, ?>) executeExpression(s)).get("foo")).getBar().getName()));
  }

  public void testSataticClassImportViaFactoryAndWithModification() {
    OptimizerFactory.setDefaultOptimizer("ASM");
    MapVariableResolverFactory mvf = new MapVariableResolverFactory(createTestMap());
    ClassImportResolverFactory classes = new ClassImportResolverFactory(null, null, false);
    classes.addClass(Person.class);

    ResolverTools.appendFactory(mvf,
        classes);

    assertEquals(21,
        executeExpression(
            compileExpression("p = new Person('tom'); p.age = 20; " +
                "with( p ) { age = p.age + 1 }; return p.age;",
                classes.getImportedClasses()),
            mvf));
  }


  public void testExecuteCoercionTwice() {
    OptimizerFactory.setDefaultOptimizer("reflective");

    Map<String, Object> vars = new HashMap<String, Object>();
    vars.put("foo",
        new Foo());
    vars.put("$value",
        new Long(5));

    ParserContext ctx = new ParserContext();
    ctx.setSourceFile("test.mv");
    ctx.setDebugSymbols(true);

    ExpressionCompiler compiler = new ExpressionCompiler("with (foo) { countTest = $value };", ctx);
    CompiledExpression compiled = compiler.compile();

    executeExpression(compiled, null, vars);
    executeExpression(compiled, null, vars);
  }

  public void testExecuteCoercionTwice2() {
    OptimizerFactory.setDefaultOptimizer("ASM");

    Map<String, Object> vars = new HashMap<String, Object>();
    vars.put("foo",
        new Foo());
    vars.put("$value",
        new Long(5));

    ParserContext ctx = new ParserContext();
    ctx.setSourceFile("test.mv");
    ctx.setDebugSymbols(true);

    ExpressionCompiler compiler = new ExpressionCompiler("with (foo) { countTest = $value };", ctx);
    CompiledExpression compiled = compiler.compile();

    executeExpression(compiled,
        null,
        vars);
    executeExpression(compiled,
        null,
        vars);
  }


  public void testNestedWithInList() {
    Recipient recipient1 = new Recipient();
    recipient1.setName("userName1");
    recipient1.setEmail("user1@domain.com");

    Recipient recipient2 = new Recipient();
    recipient2.setName("userName2");
    recipient2.setEmail("user2@domain.com");

    List<Recipient> list = new ArrayList<Recipient>();
    list.add(recipient1);
    list.add(recipient2);

    String text = "array = [" + "(with ( new Recipient() ) {name = 'userName1', email = 'user1@domain.com' }),"
        + "(with ( new Recipient() ) {name = 'userName2', email = 'user2@domain.com' })];\n";

    ParserContext context = new ParserContext();
    context.addImport(Recipient.class);

    ExpressionCompiler compiler = new ExpressionCompiler(text, context);
    Serializable execution = compiler.compile();
    List<?> result = (List<?>) executeExpression(execution,
        new HashMap<String, Object>());
    assertEquals(list,
        result);
  }

  public void testNestedWithInComplexGraph3() {
    Recipients recipients = new Recipients();

    Recipient recipient1 = new Recipient();
    recipient1.setName("user1");
    recipient1.setEmail("user1@domain.com");
    recipients.addRecipient(recipient1);

    Recipient recipient2 = new Recipient();
    recipient2.setName("user2");
    recipient2.setEmail("user2@domain.com");
    recipients.addRecipient(recipient2);

    EmailMessage msg = new EmailMessage();
    msg.setRecipients(recipients);
    msg.setFrom("from@domain.com");

    String text = "";
    text += "new EmailMessage().{ ";
    text += "     recipients = new Recipients().{ ";
    text += "         recipients = [ new Recipient().{ name = 'user1', email = 'user1@domain.com' }, ";
    text += "                        new Recipient().{ name = 'user2', email = 'user2@domain.com' } ] ";
    text += "     }, ";
    text += "     from = 'from@domain.com' }";
    ParserContext context;
    context = new ParserContext();
    context.addImport(Recipient.class);
    context.addImport(Recipients.class);
    context.addImport(EmailMessage.class);

    OptimizerFactory.setDefaultOptimizer("ASM");

    ExpressionCompiler compiler = new ExpressionCompiler(text, context);
    Serializable execution = compiler.compile();

    assertEquals(msg,
        executeExpression(execution));
    assertEquals(msg,
        executeExpression(execution));
    assertEquals(msg,
        executeExpression(execution));

    OptimizerFactory.setDefaultOptimizer("reflective");

    context = new ParserContext(context.getParserConfiguration());
    compiler = new ExpressionCompiler(text, context);
    execution = compiler.compile();

    assertEquals(msg,
        executeExpression(execution));
    assertEquals(msg,
        executeExpression(execution));
    assertEquals(msg,
        executeExpression(execution));
  }

  public void testInlineWithWithLiteral() {
    String expr = "'foo'.{ toString() }";
    assertEquals("foo", MVEL.eval(expr));

    Serializable s = MVEL.compileExpression(expr);

    assertEquals("foo", MVEL.executeExpression(s));
  }

  public void testInlineWithWithLiteralASM() throws Throwable {
      OptimizerFactory.setDefaultOptimizer("ASM");
      testInlineWithWithLiteral();
  }

  public void testInlineWithWithLiteral2() {
    String expr = "'foo'.{ toString() . toString() . toString() }";
    assertEquals("foo", MVEL.eval(expr));

    Serializable s = MVEL.compileExpression(expr);

    assertEquals("foo", MVEL.executeExpression(s));
  }

  public void testWithAndEnumInPackageImport() {
    ParserConfiguration pconf = new ParserConfiguration();
    pconf.addPackageImport(MyEnum.class.getPackage().getName());

    ParserContext pCtx = new ParserContext(pconf);
    pCtx.setStrongTyping(true);

    pCtx.addInput("thing", Thing.class);

    Thing thing = new Thing("xxx");
    Map<String, Object> vars = new HashMap<String, Object>();
    vars.put("thing", thing);

    ExecutableStatement stmt = (ExecutableStatement) MVEL.compileExpression("with( thing ) { myEnum = MyEnum.FULL_DOCUMENTATION }", pCtx);
    MVEL.executeExpression(stmt, null, vars);
    assertEquals(MyEnum.FULL_DOCUMENTATION, thing.getMyEnum());
  }

  public static class Recipient {
    private String name;
    private String email;

    public String getName() {
      return name;
    }

    public void setName(String name) {
      this.name = name;
    }

    public String getEmail() {
      return email;
    }

    public void setEmail(String email) {
      this.email = email;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((email == null) ? 0 : email.hashCode());
      result = prime * result + ((name == null) ? 0 : name.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null) return false;
      if (getClass() != obj.getClass()) return false;
      final Recipient other = (Recipient) obj;
      if (email == null) {
        if (other.email != null) return false;
      }
      else if (!email.equals(other.email)) return false;
      if (name == null) {
        if (other.name != null) return false;
      }
      else if (!name.equals(other.name)) return false;
      return true;
    }
  }

  public static class Recipients {
    private List<Recipient> list = Collections.emptyList();

    public void setRecipients(List<Recipient> recipients) {
      this.list = recipients;
    }

    public boolean addRecipient(Recipient recipient) {
      if (list == Collections.EMPTY_LIST) {
        this.list = new ArrayList<Recipient>();
      }

      if (!this.list.contains(recipient)) {
        this.list.add(recipient);
        return true;
      }
      return false;
    }

    public boolean removeRecipient(Recipient recipient) {
      return this.list.remove(recipient);
    }

    public List<Recipient> getRecipients() {
      return this.list;
    }

    public Recipient[] toArray() {
      return list.toArray(new Recipient[list.size()]);
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((list == null) ? 0 : list.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {
      if (this == obj) return true;
      if (obj == null) return false;
      if (getClass() != obj.getClass()) return false;
      final Recipients other = (Recipients) obj;
      if (list == null) {
        if (other.list != null) return false;
      }

      return list.equals(other.list);
    }
  }

  public static class EmailMessage {
    private Recipients recipients;
    private String from;

    public EmailMessage() {
    }

    public Recipients getRecipients() {
      return recipients;
    }

    public void setRecipients(Recipients recipients) {
      this.recipients = recipients;
    }

    public String getFrom() {
      return from;
    }

    public void setFrom(String from) {
      this.from = from;
    }

    @Override
    public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result + ((from == null) ? 0 : from.hashCode());
      result = prime * result + ((recipients == null) ? 0 : recipients.hashCode());
      return result;
    }

    @Override
    public boolean equals(Object obj) {

      if (this == obj) return true;
      if (obj == null) return false;
      if (getClass() != obj.getClass()) return false;
      final EmailMessage other = (EmailMessage) obj;
      if (from == null) {
        if (other.from != null) return false;
      }
      else if (!from.equals(other.from)) return false;
      if (recipients == null) {
        if (other.recipients != null) return false;
      }
      else if (!recipients.equals(other.recipients)) return false;
      return true;
    }
  }
}
