/*******************************************************************************
 * Copyright (c) 2009 Luaj.org. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/
package org.luaj.vm2;

import java.lang.reflect.InvocationTargetException;

import junit.framework.TestCase;

import org.luaj.vm2.libs.TwoArgFunction;

/**
 * Tests of basic unary and binary operators on main value types.
 */
public class UnaryBinaryOperatorsTest extends TestCase {

	LuaValue dummy;
	
	protected void setUp() throws Exception {
		super.setUp();
		dummy = LuaValue.ZERO;
	}

	public void testEqualsBool() {
		assertEquals(LuaValue.FALSE,LuaValue.FALSE);
		assertEquals(LuaValue.TRUE,LuaValue.TRUE);
		assertTrue(LuaValue.FALSE.equals(LuaValue.FALSE));
		assertTrue(LuaValue.TRUE.equals(LuaValue.TRUE));
		assertTrue(!LuaValue.FALSE.equals(LuaValue.TRUE));
		assertTrue(!LuaValue.TRUE.equals(LuaValue.FALSE));
		assertTrue(LuaValue.FALSE.eq_b(LuaValue.FALSE));
		assertTrue(LuaValue.TRUE.eq_b(LuaValue.TRUE));
		assertFalse(LuaValue.FALSE.eq_b(LuaValue.TRUE));
		assertFalse(LuaValue.TRUE.eq_b(LuaValue.FALSE));
		assertEquals(LuaValue.TRUE, LuaValue.FALSE.eq(LuaValue.FALSE));
		assertEquals(LuaValue.TRUE, LuaValue.TRUE.eq(LuaValue.TRUE));
		assertEquals(LuaValue.FALSE, LuaValue.FALSE.eq(LuaValue.TRUE));
		assertEquals(LuaValue.FALSE, LuaValue.TRUE.eq(LuaValue.FALSE));
		assertFalse(LuaValue.FALSE.neq_b(LuaValue.FALSE));
		assertFalse(LuaValue.TRUE.neq_b(LuaValue.TRUE));
		assertTrue(LuaValue.FALSE.neq_b(LuaValue.TRUE));
		assertTrue(LuaValue.TRUE.neq_b(LuaValue.FALSE));
		assertEquals(LuaValue.FALSE, LuaValue.FALSE.neq(LuaValue.FALSE));
		assertEquals(LuaValue.FALSE, LuaValue.TRUE.neq(LuaValue.TRUE));
		assertEquals(LuaValue.TRUE, LuaValue.FALSE.neq(LuaValue.TRUE));
		assertEquals(LuaValue.TRUE, LuaValue.TRUE.neq(LuaValue.FALSE));
		assertTrue(LuaValue.TRUE.toboolean());
		assertFalse(LuaValue.FALSE.toboolean());
	}
	
	public void testNot() {
		LuaValue ia=LuaValue.valueOf(3);
		LuaValue da=LuaValue.valueOf(.25);
		LuaValue sa=LuaValue.valueOf("1.5");
		LuaValue ba=LuaValue.TRUE, bb=LuaValue.FALSE;
		
		// like kinds
		assertEquals(LuaValue.FALSE,   ia.not());
		assertEquals(LuaValue.FALSE,   da.not());
		assertEquals(LuaValue.FALSE,   sa.not());
		assertEquals(LuaValue.FALSE,   ba.not());
		assertEquals(LuaValue.TRUE,    bb.not());
	}

	public void testNeg() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(-4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(-.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("-2.0");
		
		// like kinds
		assertEquals(-3., ia.neg().todouble());
		assertEquals(-.25,  da.neg().todouble());
		assertEquals(-1.5,  sa.neg().todouble());
		assertEquals(4.,  ib.neg().todouble());
		assertEquals(.5,    db.neg().todouble());
		assertEquals(2.0,   sb.neg().todouble());
	}
	
	public void testDoublesBecomeInts() {
		// DoubleValue.valueOf should return int
		LuaValue ia=LuaInteger.valueOf(345), da=LuaDouble.valueOf(345.0), db=LuaDouble.valueOf(345.5);
		LuaValue sa=LuaValue.valueOf("3.0"), sb=LuaValue.valueOf("3"), sc=LuaValue.valueOf("-2.0"), sd=LuaValue.valueOf("-2");
		
		assertEquals(ia,da);
		assertTrue(ia instanceof LuaInteger);
		assertTrue(da instanceof LuaInteger);
		assertTrue(db instanceof LuaDouble);
		assertEquals( ia.toint(), 345 );
		assertEquals( da.toint(), 345 );
		assertEquals( da.todouble(), 345.0 );
		assertEquals( db.todouble(), 345.5 );
		
		assertTrue(sa instanceof LuaString);
		assertTrue(sb instanceof LuaString);
		assertTrue(sc instanceof LuaString);
		assertTrue(sd instanceof LuaString);
		assertEquals( 3.,  sa.todouble() );
		assertEquals( 3.,  sb.todouble() );
		assertEquals( -2., sc.todouble() );
		assertEquals( -2., sd.todouble() );
		
	}
	

	public void testEqualsInt() {
		LuaValue ia=LuaInteger.valueOf(345), ib=LuaInteger.valueOf(345), ic=LuaInteger.valueOf(-345);
		LuaString sa=LuaString.valueOf("345"), sb=LuaString.valueOf("345"), sc=LuaString.valueOf("-345");
		
		// objects should be different
		assertNotSame(ia, ib);
		assertSame(sa, sb);
		assertNotSame(ia, ic);
		assertNotSame(sa, sc);
		
		// assert equals for same type
		assertEquals(ia, ib);
		assertEquals(sa, sb);
		assertFalse(ia.equals(ic));
		assertFalse(sa.equals(sc));
		
		// check object equality for different types
		assertFalse(ia.equals(sa));
		assertFalse(sa.equals(ia));
	}
		
	public void testEqualsDouble() {
		LuaValue da=LuaDouble.valueOf(345.5), db=LuaDouble.valueOf(345.5), dc=LuaDouble.valueOf(-345.5);
		LuaString sa=LuaString.valueOf("345.5"), sb=LuaString.valueOf("345.5"), sc=LuaString.valueOf("-345.5");
		
		// objects should be different
		assertNotSame(da, db);
		assertSame(sa, sb);
		assertNotSame(da, dc);
		assertNotSame(sa, sc);
		
		// assert equals for same type
		assertEquals(da, db);
		assertEquals(sa, sb);
		assertFalse(da.equals(dc));
		assertFalse(sa.equals(sc));
		
		// check object equality for different types
		assertFalse(da.equals(sa));
		assertFalse(sa.equals(da));
	}
	
	public void testEqInt() {
		LuaValue ia=LuaInteger.valueOf(345), ib=LuaInteger.valueOf(345), ic=LuaInteger.valueOf(-123);
		LuaValue sa=LuaString.valueOf("345"), sb=LuaString.valueOf("345"), sc=LuaString.valueOf("-345");
			
		// check arithmetic equality among same types
		assertEquals(ia.eq(ib),LuaValue.TRUE);
		assertEquals(sa.eq(sb),LuaValue.TRUE);
		assertEquals(ia.eq(ic),LuaValue.FALSE);
		assertEquals(sa.eq(sc),LuaValue.FALSE);

		// check arithmetic equality among different types
		assertEquals(ia.eq(sa),LuaValue.FALSE);
		assertEquals(sa.eq(ia),LuaValue.FALSE);

		// equals with mismatched types
		LuaValue t = new LuaTable();
		assertEquals(ia.eq(t),LuaValue.FALSE);
		assertEquals(t.eq(ia),LuaValue.FALSE);
		assertEquals(ia.eq(LuaValue.FALSE),LuaValue.FALSE);
		assertEquals(LuaValue.FALSE.eq(ia),LuaValue.FALSE);
		assertEquals(ia.eq(LuaValue.NIL),LuaValue.FALSE);
		assertEquals(LuaValue.NIL.eq(ia),LuaValue.FALSE);
	}
	
	public void testEqDouble() {
		LuaValue da=LuaDouble.valueOf(345.5), db=LuaDouble.valueOf(345.5), dc=LuaDouble.valueOf(-345.5);
		LuaValue sa=LuaString.valueOf("345.5"), sb=LuaString.valueOf("345.5"), sc=LuaString.valueOf("-345.5");
			
		// check arithmetic equality among same types
		assertEquals(da.eq(db),LuaValue.TRUE);
		assertEquals(sa.eq(sb),LuaValue.TRUE);
		assertEquals(da.eq(dc),LuaValue.FALSE);
		assertEquals(sa.eq(sc),LuaValue.FALSE);

		// check arithmetic equality among different types
		assertEquals(da.eq(sa),LuaValue.FALSE);
		assertEquals(sa.eq(da),LuaValue.FALSE);

		// equals with mismatched types
		LuaValue t = new LuaTable();
		assertEquals(da.eq(t),LuaValue.FALSE);
		assertEquals(t.eq(da),LuaValue.FALSE);
		assertEquals(da.eq(LuaValue.FALSE),LuaValue.FALSE);
		assertEquals(LuaValue.FALSE.eq(da),LuaValue.FALSE);
		assertEquals(da.eq(LuaValue.NIL),LuaValue.FALSE);
		assertEquals(LuaValue.NIL.eq(da),LuaValue.FALSE);
	}
	
	private static final TwoArgFunction RETURN_NIL = new TwoArgFunction() {
		public LuaValue call(LuaValue lhs, LuaValue rhs) {
			return NIL;
		}
	};
	
	private static final TwoArgFunction RETURN_ONE = new TwoArgFunction() {
		public LuaValue call(LuaValue lhs, LuaValue rhs) {
			return ONE;
		}
	};
	
	
	public void testEqualsMetatag() {
		LuaValue tru = LuaValue.TRUE;
		LuaValue fal = LuaValue.FALSE;
		LuaValue zer = LuaValue.ZERO;
		LuaValue one = LuaValue.ONE;
		LuaValue abc = LuaValue.valueOf("abcdef").substring(0,3);
		LuaValue def = LuaValue.valueOf("abcdef").substring(3,6);
		LuaValue pi  = LuaValue.valueOf(Math.PI);
		LuaValue ee  = LuaValue.valueOf(Math.E);
		LuaValue tbl = new LuaTable();
		LuaValue tbl2 = new LuaTable();
		LuaValue tbl3 = new LuaTable();
		LuaValue uda  = new LuaUserdata(new Object());
		LuaValue udb  = new LuaUserdata(uda.touserdata());
		LuaValue uda2 = new LuaUserdata(new Object());
		LuaValue uda3 = new LuaUserdata(uda.touserdata());
		LuaValue nilb = LuaValue.valueOf( LuaValue.NIL.toboolean() );
		LuaValue oneb = LuaValue.valueOf( LuaValue.ONE.toboolean() );
		assertEquals( LuaValue.FALSE, nilb );
		assertEquals( LuaValue.TRUE, oneb );
		LuaValue smt = LuaString.s_metatable;
		try {
			// always return nil0
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } );
			LuaNumber.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } );
			LuaString.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } );
			tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			tbl2.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			uda.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			udb.setmetatable(uda.getmetatable());
			uda2.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			// diff metatag function
			tbl3.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			uda3.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			
			// primitive types or same valu do not invoke metatag as per C implementation
			assertEquals( tru, tru.eq(tru) );
			assertEquals( tru, one.eq(one) );
			assertEquals( tru, abc.eq(abc) );
			assertEquals( tru, tbl.eq(tbl) );
			assertEquals( tru, uda.eq(uda) );
			assertEquals( tru, uda.eq(udb) );
			assertEquals( fal, tru.eq(fal) );
			assertEquals( fal, fal.eq(tru) );
			assertEquals( fal, zer.eq(one) );
			assertEquals( fal, one.eq(zer) );
			assertEquals( fal, pi.eq(ee) );
			assertEquals( fal, ee.eq(pi) );
			assertEquals( fal, pi.eq(one) );
			assertEquals( fal, one.eq(pi) );
			assertEquals( fal, abc.eq(def) );
			assertEquals( fal, def.eq(abc) );
			// different types.  not comparable
			assertEquals( fal, fal.eq(tbl) );
			assertEquals( fal, tbl.eq(fal) );
			assertEquals( fal, tbl.eq(one) );
			assertEquals( fal, one.eq(tbl) );
			assertEquals( fal, fal.eq(one) );
			assertEquals( fal, one.eq(fal) );
			assertEquals( fal, abc.eq(one) );
			assertEquals( fal, one.eq(abc) );
			assertEquals( fal, tbl.eq(uda) );
			assertEquals( fal, uda.eq(tbl) );
			// same type, same value, does not invoke metatag op
			assertEquals( tru, tbl.eq(tbl) );
			// same type, different value, same metatag op.  comparabile via metatag op
			assertEquals( nilb, tbl.eq(tbl2) );
			assertEquals( nilb, tbl2.eq(tbl) );
			assertEquals( nilb, uda.eq(uda2) );
			assertEquals( nilb, uda2.eq(uda) );
			// same type, different metatag ops.  not comparable
			assertEquals( fal, tbl.eq(tbl3) );
			assertEquals( fal, tbl3.eq(tbl) );
			assertEquals( fal, uda.eq(uda3) );
			assertEquals( fal, uda3.eq(uda) );

			// always use right argument
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } );
			LuaNumber.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } );
			LuaString.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } );
			tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			tbl2.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			uda.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			udb.setmetatable(uda.getmetatable());
			uda2.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_ONE, } ));
			// diff metatag function
			tbl3.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			uda3.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.EQ, RETURN_NIL, } ));
			
			// primitive types or same value do not invoke metatag as per C implementation
			assertEquals( tru, tru.eq(tru) );
			assertEquals( tru, one.eq(one) );
			assertEquals( tru, abc.eq(abc) );
			assertEquals( tru, tbl.eq(tbl) );
			assertEquals( tru, uda.eq(uda) );
			assertEquals( tru, uda.eq(udb) );
			assertEquals( fal, tru.eq(fal) );
			assertEquals( fal, fal.eq(tru) );
			assertEquals( fal, zer.eq(one) );
			assertEquals( fal, one.eq(zer) );
			assertEquals( fal, pi.eq(ee) );
			assertEquals( fal, ee.eq(pi) );
			assertEquals( fal, pi.eq(one) );
			assertEquals( fal, one.eq(pi) );
			assertEquals( fal, abc.eq(def) );
			assertEquals( fal, def.eq(abc) );
			// different types.  not comparable
			assertEquals( fal, fal.eq(tbl) );
			assertEquals( fal, tbl.eq(fal) );
			assertEquals( fal, tbl.eq(one) );
			assertEquals( fal, one.eq(tbl) );
			assertEquals( fal, fal.eq(one) );
			assertEquals( fal, one.eq(fal) );
			assertEquals( fal, abc.eq(one) );
			assertEquals( fal, one.eq(abc) );
			assertEquals( fal, tbl.eq(uda) );
			assertEquals( fal, uda.eq(tbl) );
			// same type, same value, does not invoke metatag op
			assertEquals( tru, tbl.eq(tbl) );
			// same type, different value, same metatag op.  comparabile via metatag op
			assertEquals( oneb, tbl.eq(tbl2) );
			assertEquals( oneb, tbl2.eq(tbl) );
			assertEquals( oneb, uda.eq(uda2) );
			assertEquals( oneb, uda2.eq(uda) );
			// same type, different metatag ops.  not comparable
			assertEquals( fal, tbl.eq(tbl3) );
			assertEquals( fal, tbl3.eq(tbl) );
			assertEquals( fal, uda.eq(uda3) );
			assertEquals( fal, uda3.eq(uda) );
			
		} finally { 
			LuaBoolean.s_metatable = null;
			LuaNumber.s_metatable = null;
			LuaString.s_metatable = smt;
		}
	}
		
	
	public void testAdd() {
		LuaValue ia=LuaValue.valueOf(111), ib=LuaValue.valueOf(44);
		LuaValue da=LuaValue.valueOf(55.25), db=LuaValue.valueOf(3.5);
		LuaValue sa=LuaValue.valueOf("22.125"), sb=LuaValue.valueOf("7.25");

		// check types
		assertTrue( ia instanceof LuaInteger );
		assertTrue( ib instanceof LuaInteger );
		assertTrue( da instanceof LuaDouble );
		assertTrue( db instanceof LuaDouble );
		assertTrue( sa instanceof LuaString );
		assertTrue( sb instanceof LuaString );
		
		// like kinds
		assertEquals(155.0,  ia.add(ib).todouble());
		assertEquals(58.75,  da.add(db).todouble());
		assertEquals(29.375, sa.add(sb).todouble());
		
		// unlike kinds
		assertEquals(166.25, ia.add(da).todouble());
		assertEquals(166.25, da.add(ia).todouble());
		assertEquals(133.125,ia.add(sa).todouble());
		assertEquals(133.125,sa.add(ia).todouble());
		assertEquals(77.375, da.add(sa).todouble());
		assertEquals(77.375, sa.add(da).todouble());
	}
	
	public void testSub() {
		LuaValue ia=LuaValue.valueOf(111), ib=LuaValue.valueOf(44);
		LuaValue da=LuaValue.valueOf(55.25), db=LuaValue.valueOf(3.5);
		LuaValue sa=LuaValue.valueOf("22.125"), sb=LuaValue.valueOf("7.25");
		
		// like kinds
		assertEquals(67.0,    ia.sub(ib).todouble());
		assertEquals(51.75,   da.sub(db).todouble());
		assertEquals(14.875,  sa.sub(sb).todouble());
		
		// unlike kinds
		assertEquals(55.75,   ia.sub(da).todouble());
		assertEquals(-55.75,  da.sub(ia).todouble());
		assertEquals(88.875,  ia.sub(sa).todouble());
		assertEquals(-88.875, sa.sub(ia).todouble());
		assertEquals(33.125,  da.sub(sa).todouble());
		assertEquals(-33.125, sa.sub(da).todouble());		
	}
	
	public void testMul() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		
		// like kinds
		assertEquals(12.0,    ia.mul(ib).todouble());
		assertEquals(.125,    da.mul(db).todouble());
		assertEquals(3.0,     sa.mul(sb).todouble());
		
		// unlike kinds
		assertEquals(.75,     ia.mul(da).todouble());
		assertEquals(.75,     da.mul(ia).todouble());
		assertEquals(4.5,     ia.mul(sa).todouble());
		assertEquals(4.5,     sa.mul(ia).todouble());
		assertEquals(.375,    da.mul(sa).todouble());
		assertEquals(.375,    sa.mul(da).todouble());		
	}
	
	public void testDiv() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		
		// like kinds
		assertEquals(3./4.,     ia.div(ib).todouble());
		assertEquals(.25/.5,    da.div(db).todouble());
		assertEquals(1.5/2.,    sa.div(sb).todouble());
		
		// unlike kinds
		assertEquals(3./.25,    ia.div(da).todouble());
		assertEquals(.25/3.,    da.div(ia).todouble());
		assertEquals(3./1.5,    ia.div(sa).todouble());
		assertEquals(1.5/3.,    sa.div(ia).todouble());
		assertEquals(.25/1.5,   da.div(sa).todouble());
		assertEquals(1.5/.25,   sa.div(da).todouble());		
	}
	
	public void testPow() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(4.), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		
		// like kinds
		assertEquals(Math.pow(3.,4.),     ia.pow(ib).todouble());
		assertEquals(Math.pow(4.,.5),    da.pow(db).todouble());
		assertEquals(Math.pow(1.5,2.),    sa.pow(sb).todouble());
		
		// unlike kinds
		assertEquals(Math.pow(3.,4.),    ia.pow(da).todouble());
		assertEquals(Math.pow(4.,3.),    da.pow(ia).todouble());
		assertEquals(Math.pow(3.,1.5),    ia.pow(sa).todouble());
		assertEquals(Math.pow(1.5,3.),    sa.pow(ia).todouble());
		assertEquals(Math.pow(4.,1.5),   da.pow(sa).todouble());
		assertEquals(Math.pow(1.5,4.),   sa.pow(da).todouble());		
	}

	private static double luaMod(double x, double y) {
		return y!=0? x-y*Math.floor(x/y): Double.NaN;
	}
	
	public void testMod() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(-4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(-.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("-2.0");
		
		// like kinds
		assertEquals(luaMod(3.,-4.),     ia.mod(ib).todouble());
		assertEquals(luaMod(.25,-.5),    da.mod(db).todouble());
		assertEquals(luaMod(1.5,-2.),    sa.mod(sb).todouble());
		
		// unlike kinds
		assertEquals(luaMod(3.,.25),    ia.mod(da).todouble());
		assertEquals(luaMod(.25,3.),    da.mod(ia).todouble());
		assertEquals(luaMod(3.,1.5),    ia.mod(sa).todouble());
		assertEquals(luaMod(1.5,3.),    sa.mod(ia).todouble());
		assertEquals(luaMod(.25,1.5),   da.mod(sa).todouble());
		assertEquals(luaMod(1.5,.25),   sa.mod(da).todouble());		
	}

	public void testArithErrors() {
		LuaValue ia=LuaValue.valueOf(111), ib=LuaValue.valueOf(44);
		LuaValue da=LuaValue.valueOf(55.25), db=LuaValue.valueOf(3.5);
		LuaValue sa=LuaValue.valueOf("22.125"), sb=LuaValue.valueOf("7.25");

		String[] ops = { "add", "sub", "mul", "div", "mod", "pow" };
		LuaValue[] vals = { LuaValue.NIL, LuaValue.TRUE, LuaValue.tableOf() };
		LuaValue[] numerics = { LuaValue.valueOf(111), LuaValue.valueOf(55.25), LuaValue.valueOf("22.125") }; 
		for ( int i=0; i<ops.length; i++ ) {
			for ( int j=0; j<vals.length; j++ ) {
				for ( int k=0; k<numerics.length; k++ ) {
					checkArithError( vals[j], numerics[k], ops[i], vals[j].typename() );
					checkArithError( numerics[k], vals[j], ops[i], vals[j].typename() );
				}
			}
		}
	}
	
	private void checkArithError(LuaValue a, LuaValue b, String op, String type) {
		try {
			LuaValue.class.getMethod(op, new Class[] { LuaValue.class }).invoke(a, new Object[] { b });
		} catch ( InvocationTargetException ite ) {
			String actual = ite.getTargetException().getMessage();
			if ( (!actual.startsWith("attempt to perform arithmetic")) || actual.indexOf(type)<0 ) 
				fail( "("+a.typename()+","+op+","+b.typename()+") reported '"+actual+"'" );
		} catch ( Exception e ) {
			fail( "("+a.typename()+","+op+","+b.typename()+") threw "+e );
		}
	}
	
	private static final TwoArgFunction RETURN_LHS = new TwoArgFunction() {
		public LuaValue call(LuaValue lhs, LuaValue rhs) {
			return lhs;
		}
	};
	
	private static final TwoArgFunction RETURN_RHS = new TwoArgFunction() {
		public LuaValue call(LuaValue lhs, LuaValue rhs) {
			return rhs;
		}
	};
	
	public void testArithMetatag() {
		LuaValue tru = LuaValue.TRUE;
		LuaValue fal = LuaValue.FALSE;
		LuaValue tbl = new LuaTable();
		LuaValue tbl2 = new LuaTable();
		try {
			try { tru.add(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.mul(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.div(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.pow(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.mod(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			
			// always use left argument
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.ADD, RETURN_LHS, } );
			assertEquals( tru, tru.add(fal) );
			assertEquals( tru, tru.add(tbl) );
			assertEquals( tbl, tbl.add(tru) );
			try { tbl.add(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.SUB, RETURN_LHS, } );
			assertEquals( tru, tru.sub(fal) );
			assertEquals( tru, tru.sub(tbl) );
			assertEquals( tbl, tbl.sub(tru) );
			try { tbl.sub(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.add(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.MUL, RETURN_LHS, } );
			assertEquals( tru, tru.mul(fal) );
			assertEquals( tru, tru.mul(tbl) );
			assertEquals( tbl, tbl.mul(tru) );
			try { tbl.mul(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.DIV, RETURN_LHS, } );
			assertEquals( tru, tru.div(fal) );
			assertEquals( tru, tru.div(tbl) );
			assertEquals( tbl, tbl.div(tru) );
			try { tbl.div(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.POW, RETURN_LHS, } );
			assertEquals( tru, tru.pow(fal) );
			assertEquals( tru, tru.pow(tbl) );
			assertEquals( tbl, tbl.pow(tru) );
			try { tbl.pow(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.MOD, RETURN_LHS, } );
			assertEquals( tru, tru.mod(fal) );
			assertEquals( tru, tru.mod(tbl) );
			assertEquals( tbl, tbl.mod(tru) );
			try { tbl.mod(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			
			// always use right argument
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.ADD, RETURN_RHS, } );
			assertEquals( fal, tru.add(fal) );
			assertEquals( tbl, tru.add(tbl) );
			assertEquals( tru, tbl.add(tru) );
			try { tbl.add(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.SUB, RETURN_RHS, } );
			assertEquals( fal, tru.sub(fal) );
			assertEquals( tbl, tru.sub(tbl) );
			assertEquals( tru, tbl.sub(tru) );
			try { tbl.sub(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.add(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.MUL, RETURN_RHS, } );
			assertEquals( fal, tru.mul(fal) );
			assertEquals( tbl, tru.mul(tbl) );
			assertEquals( tru, tbl.mul(tru) );
			try { tbl.mul(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.DIV, RETURN_RHS, } );
			assertEquals( fal, tru.div(fal) );
			assertEquals( tbl, tru.div(tbl) );
			assertEquals( tru, tbl.div(tru) );
			try { tbl.div(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.POW, RETURN_RHS, } );
			assertEquals( fal, tru.pow(fal) );
			assertEquals( tbl, tru.pow(tbl) );
			assertEquals( tru, tbl.pow(tru) );
			try { tbl.pow(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };

			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.MOD, RETURN_RHS, } );
			assertEquals( fal, tru.mod(fal) );
			assertEquals( tbl, tru.mod(tbl) );
			assertEquals( tru, tbl.mod(tru) );
			try { tbl.mod(tbl2); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tru.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };


		} finally { 
			LuaBoolean.s_metatable = null;
		}
	}	
	
	public void testArithMetatagNumberTable() {
		LuaValue zero = LuaValue.ZERO;
		LuaValue one = LuaValue.ONE;
		LuaValue tbl = new LuaTable();

		try { tbl.add(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.add(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.ADD, RETURN_ONE, } ));
		assertEquals( one, tbl.add(zero) );
		assertEquals( one, zero.add(tbl) );

		try { tbl.sub(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.sub(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.SUB, RETURN_ONE, } ));
		assertEquals( one, tbl.sub(zero) );
		assertEquals( one, zero.sub(tbl) );

		try { tbl.mul(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.mul(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.MUL, RETURN_ONE, } ));
		assertEquals( one, tbl.mul(zero) );
		assertEquals( one, zero.mul(tbl) );
		
		try { tbl.div(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.div(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.DIV, RETURN_ONE, } ));
		assertEquals( one, tbl.div(zero) );
		assertEquals( one, zero.div(tbl) );

		try { tbl.pow(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.pow(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.POW, RETURN_ONE, } ));
		assertEquals( one, tbl.pow(zero) );
		assertEquals( one, zero.pow(tbl) );

		try { tbl.mod(zero); fail("did not throw error"); } catch ( LuaError le ) { };
		try { zero.mod(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
		tbl.setmetatable(LuaValue.tableOf( new LuaValue[] { LuaValue.MOD, RETURN_ONE, } ));
		assertEquals( one, tbl.mod(zero) );
		assertEquals( one, zero.mod(tbl) );
	}
	
	public void testCompareStrings() {
		// these are lexical compare!
		LuaValue sa=LuaValue.valueOf("-1.5");
		LuaValue sb=LuaValue.valueOf("-2.0");
		LuaValue sc=LuaValue.valueOf("1.5");
		LuaValue sd=LuaValue.valueOf("2.0");
		
		assertEquals(LuaValue.FALSE,    sa.lt(sa));
		assertEquals(LuaValue.TRUE,     sa.lt(sb));
		assertEquals(LuaValue.TRUE,     sa.lt(sc));
		assertEquals(LuaValue.TRUE,     sa.lt(sd));
		assertEquals(LuaValue.FALSE,    sb.lt(sa));
		assertEquals(LuaValue.FALSE,    sb.lt(sb));
		assertEquals(LuaValue.TRUE,     sb.lt(sc));
		assertEquals(LuaValue.TRUE,     sb.lt(sd));
		assertEquals(LuaValue.FALSE,    sc.lt(sa));
		assertEquals(LuaValue.FALSE,    sc.lt(sb));
		assertEquals(LuaValue.FALSE,    sc.lt(sc));
		assertEquals(LuaValue.TRUE,     sc.lt(sd));
		assertEquals(LuaValue.FALSE,    sd.lt(sa));
		assertEquals(LuaValue.FALSE,    sd.lt(sb));
		assertEquals(LuaValue.FALSE,    sd.lt(sc));
		assertEquals(LuaValue.FALSE,    sd.lt(sd));
	}
	
	public void testLt() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		
		// like kinds
		assertEquals(3.<4.,     ia.lt(ib).toboolean());
		assertEquals(.25<.5,    da.lt(db).toboolean());
		assertEquals(3.<4.,     ia.lt_b(ib));
		assertEquals(.25<.5,    da.lt_b(db));
		
		// unlike kinds
		assertEquals(3.<.25,    ia.lt(da).toboolean());
		assertEquals(.25<3.,    da.lt(ia).toboolean());
		assertEquals(3.<.25,    ia.lt_b(da));
		assertEquals(.25<3.,    da.lt_b(ia));
	}
	
	public void testLtEq() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		
		// like kinds
		assertEquals(3.<=4.,     ia.lteq(ib).toboolean());
		assertEquals(.25<=.5,    da.lteq(db).toboolean());
		assertEquals(3.<=4.,     ia.lteq_b(ib));
		assertEquals(.25<=.5,    da.lteq_b(db));
		
		// unlike kinds
		assertEquals(3.<=.25,    ia.lteq(da).toboolean());
		assertEquals(.25<=3.,    da.lteq(ia).toboolean());
		assertEquals(3.<=.25,    ia.lteq_b(da));
		assertEquals(.25<=3.,    da.lteq_b(ia));
	}
	
	public void testGt() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		
		// like kinds
		assertEquals(3.>4.,     ia.gt(ib).toboolean());
		assertEquals(.25>.5,    da.gt(db).toboolean());
		assertEquals(3.>4.,     ia.gt_b(ib));
		assertEquals(.25>.5,    da.gt_b(db));
		
		// unlike kinds
		assertEquals(3.>.25,    ia.gt(da).toboolean());
		assertEquals(.25>3.,    da.gt(ia).toboolean());
		assertEquals(3.>.25,    ia.gt_b(da));
		assertEquals(.25>3.,    da.gt_b(ia));
	}
	
	public void testGtEq() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		
		// like kinds
		assertEquals(3.>=4.,     ia.gteq(ib).toboolean());
		assertEquals(.25>=.5,    da.gteq(db).toboolean());
		assertEquals(3.>=4.,     ia.gteq_b(ib));
		assertEquals(.25>=.5,    da.gteq_b(db));
		
		// unlike kinds
		assertEquals(3.>=.25,    ia.gteq(da).toboolean());
		assertEquals(.25>=3.,    da.gteq(ia).toboolean());
		assertEquals(3.>=.25,    ia.gteq_b(da));
		assertEquals(.25>=3.,    da.gteq_b(ia));
	}

	public void testNotEq() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		
		// like kinds
		assertEquals(3.!=4.,     ia.neq(ib).toboolean());
		assertEquals(.25!=.5,    da.neq(db).toboolean());
		assertEquals(1.5!=2.,    sa.neq(sb).toboolean());
		assertEquals(3.!=4.,     ia.neq_b(ib));
		assertEquals(.25!=.5,    da.neq_b(db));
		assertEquals(1.5!=2.,    sa.neq_b(sb));
		
		// unlike kinds
		assertEquals(3.!=.25,    ia.neq(da).toboolean());
		assertEquals(.25!=3.,    da.neq(ia).toboolean());
		assertEquals(3.!=1.5,    ia.neq(sa).toboolean());
		assertEquals(1.5!=3.,    sa.neq(ia).toboolean());
		assertEquals(.25!=1.5,   da.neq(sa).toboolean());
		assertEquals(1.5!=.25,   sa.neq(da).toboolean());		
		assertEquals(3.!=.25,    ia.neq_b(da));
		assertEquals(.25!=3.,    da.neq_b(ia));
		assertEquals(3.!=1.5,    ia.neq_b(sa));
		assertEquals(1.5!=3.,    sa.neq_b(ia));
		assertEquals(.25!=1.5,   da.neq_b(sa));
		assertEquals(1.5!=.25,   sa.neq_b(da));		
	}


	public void testCompareErrors() {
		LuaValue ia=LuaValue.valueOf(111), ib=LuaValue.valueOf(44);
		LuaValue da=LuaValue.valueOf(55.25), db=LuaValue.valueOf(3.5);
		LuaValue sa=LuaValue.valueOf("22.125"), sb=LuaValue.valueOf("7.25");

		String[] ops = { "lt", "lteq",  };		
		LuaValue[] vals = { LuaValue.NIL, LuaValue.TRUE, LuaValue.tableOf() };
		LuaValue[] numerics = { LuaValue.valueOf(111), LuaValue.valueOf(55.25), LuaValue.valueOf("22.125") }; 
		for ( int i=0; i<ops.length; i++ ) {
			for ( int j=0; j<vals.length; j++ ) {
				for ( int k=0; k<numerics.length; k++ ) {
					checkCompareError( vals[j], numerics[k], ops[i], vals[j].typename() );
					checkCompareError( numerics[k], vals[j], ops[i], vals[j].typename() );
				}
			}
		}
	}
	
	private void checkCompareError(LuaValue a, LuaValue b, String op, String type) {
		try {
			LuaValue.class.getMethod(op, new Class[] { LuaValue.class }).invoke(a, new Object[] { b });
		} catch ( InvocationTargetException ite ) {
			String actual = ite.getTargetException().getMessage();
			if ( (!actual.startsWith("attempt to compare")) || actual.indexOf(type)<0 ) 
				fail( "("+a.typename()+","+op+","+b.typename()+") reported '"+actual+"'" );
		} catch ( Exception e ) {
			fail( "("+a.typename()+","+op+","+b.typename()+") threw "+e );
		}
	}
	
	public void testCompareMetatag() {
		LuaValue tru = LuaValue.TRUE;
		LuaValue fal = LuaValue.FALSE;
		LuaValue tbl = new LuaTable();
		LuaValue tbl2 = new LuaTable();
		LuaValue tbl3 = new LuaTable();
		try {
			// always use left argument
			LuaValue mt = LuaValue.tableOf( new LuaValue[] { 
					LuaValue.LT, RETURN_LHS, 
					LuaValue.LE, RETURN_RHS, 
					} );
			LuaBoolean.s_metatable = mt;
			tbl.setmetatable(mt);
			tbl2.setmetatable(mt);
			assertEquals( tru, tru.lt(fal) );
			assertEquals( fal, fal.lt(tru) );
			assertEquals( tbl, tbl.lt(tbl2) );
			assertEquals( tbl2, tbl2.lt(tbl) );
			assertEquals( tbl, tbl.lt(tbl3) );
			assertEquals( tbl3, tbl3.lt(tbl) );
			assertEquals( fal, tru.lteq(fal) );
			assertEquals( tru, fal.lteq(tru) );
			assertEquals( tbl2, tbl.lteq(tbl2) );
			assertEquals( tbl, tbl2.lteq(tbl) );
			assertEquals( tbl3, tbl.lteq(tbl3) );
			assertEquals( tbl, tbl3.lteq(tbl) );

			// always use right argument
			mt = LuaValue.tableOf( new LuaValue[] { 
					LuaValue.LT, RETURN_RHS,
					LuaValue.LE, RETURN_LHS } );
			LuaBoolean.s_metatable = mt;
			tbl.setmetatable(mt);
			tbl2.setmetatable(mt);
			assertEquals( fal, tru.lt(fal) );
			assertEquals( tru, fal.lt(tru) );
			assertEquals( tbl2, tbl.lt(tbl2) );
			assertEquals( tbl, tbl2.lt(tbl) );
			assertEquals( tbl3, tbl.lt(tbl3) );
			assertEquals( tbl, tbl3.lt(tbl) );
			assertEquals( tru, tru.lteq(fal) );
			assertEquals( fal, fal.lteq(tru) );
			assertEquals( tbl, tbl.lteq(tbl2) );
			assertEquals( tbl2, tbl2.lteq(tbl) );
			assertEquals( tbl, tbl.lteq(tbl3) );
			assertEquals( tbl3, tbl3.lteq(tbl) );
			
		} finally { 
			LuaBoolean.s_metatable = null;
		}
	}
		
	public void testAnd() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		LuaValue ba=LuaValue.TRUE, bb=LuaValue.FALSE;
		
		// like kinds
		assertSame(ib,   ia.and(ib));
		assertSame(db,   da.and(db));
		assertSame(sb,   sa.and(sb));
		
		// unlike kinds
		assertSame(da,   ia.and(da));
		assertSame(ia,   da.and(ia));
		assertSame(sa,   ia.and(sa));
		assertSame(ia,   sa.and(ia));
		assertSame(sa,   da.and(sa));
		assertSame(da,   sa.and(da));
		
		// boolean values
		assertSame(bb,   ba.and(bb));
		assertSame(bb,   bb.and(ba));
		assertSame(ia,   ba.and(ia));
		assertSame(bb,   bb.and(ia));
	}


	public void testOr() {
		LuaValue ia=LuaValue.valueOf(3), ib=LuaValue.valueOf(4);
		LuaValue da=LuaValue.valueOf(.25), db=LuaValue.valueOf(.5);
		LuaValue sa=LuaValue.valueOf("1.5"), sb=LuaValue.valueOf("2.0");
		LuaValue ba=LuaValue.TRUE, bb=LuaValue.FALSE;
		
		// like kinds
		assertSame(ia,   ia.or(ib));
		assertSame(da,   da.or(db));
		assertSame(sa,   sa.or(sb));
		
		// unlike kinds
		assertSame(ia,   ia.or(da));
		assertSame(da,   da.or(ia));
		assertSame(ia,   ia.or(sa));
		assertSame(sa,   sa.or(ia));
		assertSame(da,   da.or(sa));
		assertSame(sa,   sa.or(da));		
		
		// boolean values
		assertSame(ba,   ba.or(bb));
		assertSame(ba,   bb.or(ba));
		assertSame(ba,   ba.or(ia));
		assertSame(ia,   bb.or(ia));
	}
	
	public void testLexicalComparison() {
		LuaValue aaa = LuaValue.valueOf("aaa");
		LuaValue baa = LuaValue.valueOf("baa");
		LuaValue Aaa = LuaValue.valueOf("Aaa");
		LuaValue aba = LuaValue.valueOf("aba");
		LuaValue aaaa = LuaValue.valueOf("aaaa");
		LuaValue t = LuaValue.TRUE;
		LuaValue f = LuaValue.FALSE;
		
		// basics
		assertEquals(t, aaa.eq(aaa));
		assertEquals(t, aaa.lt(baa));
		assertEquals(t, aaa.lteq(baa));
		assertEquals(f, aaa.gt(baa));
		assertEquals(f, aaa.gteq(baa));
		assertEquals(f, baa.lt(aaa));
		assertEquals(f, baa.lteq(aaa));
		assertEquals(t, baa.gt(aaa));
		assertEquals(t, baa.gteq(aaa));
		assertEquals(t, aaa.lteq(aaa));
		assertEquals(t, aaa.gteq(aaa));

		// different case
		assertEquals(t, Aaa.eq(Aaa));
		assertEquals(t, Aaa.lt(aaa));
		assertEquals(t, Aaa.lteq(aaa));
		assertEquals(f, Aaa.gt(aaa));
		assertEquals(f, Aaa.gteq(aaa));
		assertEquals(f, aaa.lt(Aaa));
		assertEquals(f, aaa.lteq(Aaa));
		assertEquals(t, aaa.gt(Aaa));
		assertEquals(t, aaa.gteq(Aaa));
		assertEquals(t, Aaa.lteq(Aaa));
		assertEquals(t, Aaa.gteq(Aaa));
		
		// second letter differs
		assertEquals(t, aaa.eq(aaa));
		assertEquals(t, aaa.lt(aba));
		assertEquals(t, aaa.lteq(aba));
		assertEquals(f, aaa.gt(aba));
		assertEquals(f, aaa.gteq(aba));
		assertEquals(f, aba.lt(aaa));
		assertEquals(f, aba.lteq(aaa));
		assertEquals(t, aba.gt(aaa));
		assertEquals(t, aba.gteq(aaa));
		assertEquals(t, aaa.lteq(aaa));
		assertEquals(t, aaa.gteq(aaa));
		
		// longer
		assertEquals(t, aaa.eq(aaa));
		assertEquals(t, aaa.lt(aaaa));
		assertEquals(t, aaa.lteq(aaaa));
		assertEquals(f, aaa.gt(aaaa));
		assertEquals(f, aaa.gteq(aaaa));
		assertEquals(f, aaaa.lt(aaa));
		assertEquals(f, aaaa.lteq(aaa));
		assertEquals(t, aaaa.gt(aaa));
		assertEquals(t, aaaa.gteq(aaa));
		assertEquals(t, aaa.lteq(aaa));
		assertEquals(t, aaa.gteq(aaa));
	}
	
	public void testBuffer() {
		LuaValue abc = LuaValue.valueOf("abcdefghi").substring(0,3);
		LuaValue def = LuaValue.valueOf("abcdefghi").substring(3,6);
		LuaValue ghi = LuaValue.valueOf("abcdefghi").substring(6,9);
		LuaValue n123 = LuaValue.valueOf(123);

		// basic append
		Buffer b = new Buffer(); assertEquals( "", b.value().tojstring() );
		b.append(def); assertEquals( "def", b.value().tojstring() );
		b.append(abc); assertEquals( "defabc", b.value().tojstring() );
		b.append(ghi); assertEquals( "defabcghi", b.value().tojstring() );
		b.append(n123); assertEquals( "defabcghi123", b.value().tojstring() );		

		// basic prepend
		b = new Buffer(); assertEquals( "", b.value().tojstring() );
		b.prepend(def.strvalue()); assertEquals( "def", b.value().tojstring() );
		b.prepend(ghi.strvalue()); assertEquals( "ghidef", b.value().tojstring() );
		b.prepend(abc.strvalue()); assertEquals( "abcghidef", b.value().tojstring() );
		b.prepend(n123.strvalue()); assertEquals( "123abcghidef", b.value().tojstring() );

		// mixed append, prepend
		b = new Buffer(); assertEquals( "", b.value().tojstring() );
		b.append(def); assertEquals( "def", b.value().tojstring() );
		b.append(abc); assertEquals( "defabc", b.value().tojstring() );
		b.prepend(ghi.strvalue()); assertEquals( "ghidefabc", b.value().tojstring() );
		b.prepend(n123.strvalue()); assertEquals( "123ghidefabc", b.value().tojstring() );		
		b.append(def); assertEquals( "123ghidefabcdef", b.value().tojstring() );
		b.append(abc); assertEquals( "123ghidefabcdefabc", b.value().tojstring() );
		b.prepend(ghi.strvalue()); assertEquals( "ghi123ghidefabcdefabc", b.value().tojstring() );
		b.prepend(n123.strvalue()); assertEquals( "123ghi123ghidefabcdefabc", b.value().tojstring() );		
		
		// value
		b = new Buffer(def); assertEquals( "def", b.value().tojstring() );
		b.append(abc); assertEquals( "defabc", b.value().tojstring() );
		b.prepend(ghi.strvalue()); assertEquals( "ghidefabc", b.value().tojstring() );
		b.setvalue(def); assertEquals( "def", b.value().tojstring() );
		b.prepend(ghi.strvalue()); assertEquals( "ghidef", b.value().tojstring() );
		b.append(abc); assertEquals( "ghidefabc", b.value().tojstring() );
	}
	
	public void testConcat() {
		LuaValue abc = LuaValue.valueOf("abcdefghi").substring(0,3);
		LuaValue def = LuaValue.valueOf("abcdefghi").substring(3,6);
		LuaValue ghi = LuaValue.valueOf("abcdefghi").substring(6,9);
		LuaValue n123 = LuaValue.valueOf(123);
		
		assertEquals( "abc", abc.tojstring() );
		assertEquals( "def", def.tojstring() );
		assertEquals( "ghi", ghi.tojstring() );
		assertEquals( "123", n123.tojstring() );
		assertEquals( "abcabc", abc.concat(abc).tojstring() );
		assertEquals( "defghi", def.concat(ghi).tojstring() );
		assertEquals( "ghidef", ghi.concat(def).tojstring() );
		assertEquals( "ghidefabcghi", ghi.concat(def).concat(abc).concat(ghi).tojstring() );
		assertEquals( "123def", n123.concat(def).tojstring() );
		assertEquals( "def123", def.concat(n123).tojstring() );
	}
	
	public void testConcatBuffer() {
		LuaValue abc = LuaValue.valueOf("abcdefghi").substring(0,3);
		LuaValue def = LuaValue.valueOf("abcdefghi").substring(3,6);
		LuaValue ghi = LuaValue.valueOf("abcdefghi").substring(6,9);
		LuaValue n123 = LuaValue.valueOf(123);
		Buffer b;

		b = new Buffer(def); assertEquals( "def", b.value().tojstring() );
		b = ghi.concat(b); assertEquals( "ghidef", b.value().tojstring() );
		b = abc.concat(b); assertEquals( "abcghidef", b.value().tojstring() );
		b = n123.concat(b); assertEquals( "123abcghidef", b.value().tojstring() );
		b.setvalue(n123);
		b = def.concat(b); assertEquals( "def123", b.value().tojstring() );
		b = abc.concat(b); assertEquals( "abcdef123", b.value().tojstring() );
	}
	
	public void testConcatMetatag() {
		LuaValue def = LuaValue.valueOf("abcdefghi").substring(3,6);
		LuaValue ghi = LuaValue.valueOf("abcdefghi").substring(6,9);
		LuaValue tru = LuaValue.TRUE;
		LuaValue fal = LuaValue.FALSE;
		LuaValue tbl = new LuaTable();
		LuaValue uda = new LuaUserdata(new Object());
		try {
			// always use left argument
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.CONCAT, RETURN_LHS } );
			assertEquals( tru, tru.concat(tbl) );
			assertEquals( tbl, tbl.concat(tru) );
			assertEquals( tru, tru.concat(tbl) );
			assertEquals( tbl, tbl.concat(tru) );
			assertEquals( tru, tru.concat(tbl.buffer()).value() );
			assertEquals( tbl, tbl.concat(tru.buffer()).value() );
			assertEquals( fal, fal.concat(tbl.concat(tru.buffer())).value() );
			assertEquals( uda, uda.concat(tru.concat(tbl.buffer())).value() );
			try { tbl.concat(def); fail("did not throw error"); } catch ( LuaError le ) { };
			try { def.concat(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tbl.concat(def.buffer()).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { def.concat(tbl.buffer()).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { uda.concat(def.concat(tbl.buffer())).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { ghi.concat(tbl.concat(def.buffer())).value(); fail("did not throw error"); } catch ( LuaError le ) { };

			// always use right argument
			LuaBoolean.s_metatable = LuaValue.tableOf( new LuaValue[] { LuaValue.CONCAT, RETURN_RHS } );
			assertEquals( tbl, tru.concat(tbl) );
			assertEquals( tru, tbl.concat(tru) );
			assertEquals( tbl, tru.concat(tbl.buffer()).value() );
			assertEquals( tru, tbl.concat(tru.buffer()).value() );
			assertEquals( tru, uda.concat(tbl.concat(tru.buffer())).value() );
			assertEquals( tbl, fal.concat(tru.concat(tbl.buffer())).value() );
			try { tbl.concat(def); fail("did not throw error"); } catch ( LuaError le ) { };
			try { def.concat(tbl); fail("did not throw error"); } catch ( LuaError le ) { };
			try { tbl.concat(def.buffer()).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { def.concat(tbl.buffer()).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { uda.concat(def.concat(tbl.buffer())).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			try { uda.concat(tbl.concat(def.buffer())).value(); fail("did not throw error"); } catch ( LuaError le ) { };
			
		} finally { 
			LuaBoolean.s_metatable = null;
		}
	}

	public void testConcatErrors() {
		LuaValue ia=LuaValue.valueOf(111), ib=LuaValue.valueOf(44);
		LuaValue da=LuaValue.valueOf(55.25), db=LuaValue.valueOf(3.5);
		LuaValue sa=LuaValue.valueOf("22.125"), sb=LuaValue.valueOf("7.25");

		String[] ops = { "concat" };		
		LuaValue[] vals = { LuaValue.NIL, LuaValue.TRUE, LuaValue.tableOf() };
		LuaValue[] numerics = { LuaValue.valueOf(111), LuaValue.valueOf(55.25), LuaValue.valueOf("22.125") }; 
		for ( int i=0; i<ops.length; i++ ) {
			for ( int j=0; j<vals.length; j++ ) {
				for ( int k=0; k<numerics.length; k++ ) {
					checkConcatError( vals[j], numerics[k], ops[i], vals[j].typename() );
					checkConcatError( numerics[k], vals[j], ops[i], vals[j].typename() );
				}
			}
		}
	}
	
	private void checkConcatError(LuaValue a, LuaValue b, String op, String type) {
		try {
			LuaValue.class.getMethod(op, new Class[] { LuaValue.class }).invoke(a, new Object[] { b });
		} catch ( InvocationTargetException ite ) {
			String actual = ite.getTargetException().getMessage();
			if ( (!actual.startsWith("attempt to concatenate")) || actual.indexOf(type)<0 ) 
				fail( "("+a.typename()+","+op+","+b.typename()+") reported '"+actual+"'" );
		} catch ( Exception e ) {
			fail( "("+a.typename()+","+op+","+b.typename()+") threw "+e );
		}
	}
	
}
