package com.hammurapi.common.concurrent.tests;

import static org.junit.Assert.*;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

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

import com.hammurapi.common.concurrent.Invocable;
import com.hammurapi.common.concurrent.Invocation;
import com.hammurapi.common.concurrent.LocalStringPropertySet;
import com.hammurapi.common.concurrent.PropertySet;

public class LocalStringPropertySetTests {
	
	private ExecutorService executorService;
	private ScheduledExecutorService scheduledExecutorService;

	@Before
	public void setUp() throws Exception {
		executorService = Executors.newFixedThreadPool(5);
		scheduledExecutorService = Executors.newScheduledThreadPool(5);
	}

	@After
	public void tearDown() throws Exception {
		executorService.shutdown();
		scheduledExecutorService.shutdown();
	}
	
	private LocalStringPropertySet[] createPropertySets(PropertySet<String>... chain) {
		return new LocalStringPropertySet[] {
				new LocalStringPropertySet(null, null, null, null, chain), 
				new LocalStringPropertySet(executorService, null, null, null, chain),
				new LocalStringPropertySet(scheduledExecutorService, null, null, null, chain)			
		};
	}
	
	public interface TestInterface {
	
		int getCount();
		
		void setCount(int count);
		
		int increment(int delta);
		
	}

	@Test
	public void testGetAdapter() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			ps.set("count", "33");
			
			Invocable<PropertySet<String>, Object, String, RuntimeException> invocable = new Invocable<PropertySet<String>, Object, String, RuntimeException>() {

				@Override
				public Object invoke(
						PropertySet<String> context,
						Invocation<String> invocation,
						ExecutorService executorService) {
					
					Integer current = context.get("count", Integer.class);
					Integer delta = (Integer) invocation.getArguments()[0];
					Integer value = current + delta; 
					context.set("count", value);
					return value;
				}

				@Override
				public Class getReturnType() {
					return Integer.class;
				}

				@Override
				public Class<?>[] getParameterTypes() {
					return new Class[] {Integer.class};
				}
				
			};
			
			ps.setInvocable("increment", invocable);
			
			TestInterface adapter = ps.getAdapter(TestInterface.class, null);
			
			assertEquals(33, adapter.getCount());
			
			adapter.setCount(88);
			assertEquals(88, ps.get("count"));
			
			int newCount = adapter.increment(12);
			assertEquals(100, newCount);
			assertEquals(100, ps.get("count"));		
		}
	}

	@Test
	public void testGetFromInvocable() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			
			Invocable<PropertySet<String>, Object, String, RuntimeException> invocable = new Invocable<PropertySet<String>, Object, String, RuntimeException>() {

				@Override
				public Object invoke(
						PropertySet<String> context,
						Invocation<String> invocation,
						ExecutorService executorService) {
					
					return 33;
				}

				@Override
				public Class getReturnType() {
					return Integer.class;
				}

				@Override
				public Class<?>[] getParameterTypes() {
					return new Class[] {};
				}
				
			};
			
			ps.setInvocable("closure", invocable);			
			assertEquals(33, ps.get("closure"));		
		}
	}

	@Test
	public void testKeySet() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			// Own keyset
			ps.set("v", 33);
			ps.set("z/x", 88);
			Set<String> ks = ps.keySet();
			assertEquals(2, ks.size());
			assertTrue(ks.contains("v"));
			assertTrue(ks.contains("z/x"));
			
			// subsets'
			// mounts
			// chains
		}
	}

	@Test
	public void testSubset() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			PropertySet<String> subSet = ps.subset("root");
			subSet.set("child", 33);
			assertEquals(33, ps.get("root/child"));
			ps.set("root/sibling", 88);
			assertEquals(88, subSet.get("sibling"));
		}
	}

	@Test
	public void testRemove() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			ps.set("root/sibling", 88);
			assertEquals(88, ps.get("root/sibling"));
			ps.remove("root/sibling");
			assertNull(ps.get("root/sibling"));
			
			// TODO - test with mounts.
		}
	}

	@Test
	public void testClear() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			ps.set("root/sibling", 88);
			assertEquals(88, ps.get("root/sibling"));
			ps.clear();
			assertNull(ps.get("root/sibling"));
			
			// TODO - test with mounts.
		}
	}

	@Test
	public void testMount() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			PropertySet<String> subSet = ps.subset("root");
			subSet.set("child", 33);
			subSet.set("s3", 888);
			assertEquals(33, ps.get("root/child"));
			
			LocalStringPropertySet mount = new LocalStringPropertySet(executorService, null, null, null);
			ps.mount("root", mount);
			assertNull(ps.get("root/child"));
			
			ps.set("root/sibling", 88);
			assertNull(subSet.get("sibling"));
			assertEquals(88, mount.get("sibling"));
			
			mount.set("s2", 777);
			assertEquals(777, ps.get("root/s2"));
			assertNull(ps.get("root/s3"));
			
			ps.unmount("root");
			assertNull(ps.get("root/s2"));
			assertEquals(888, ps.get("root/s3"));			
		}
	}

	@Test
	public void testSetAll() {
		for (LocalStringPropertySet ps: createPropertySets()) {			
			LocalStringPropertySet src = new LocalStringPropertySet(executorService, null, null, null);
			src.set("v1", 33);
			src.set("v2", 88);
			ps.setAll(src);
			
			assertEquals(33, ps.get("v1"));
			assertEquals(88, ps.get("v2"));
		}
	}

	@Test
	public void testGet() {
		for (LocalStringPropertySet ps: createPropertySets()) {			
			ps.set("v", 33);
			assertEquals(33, ps.get("v"));
		}
	}

	@Test
	public void testGetConverted() {
		for (LocalStringPropertySet ps: createPropertySets()) {			
			ps.set("v", "33");
			assertEquals(33, (Object) ps.get("v", Integer.class));
		}
	}

	@Test
	public void testGetDefaultValue() {
		for (LocalStringPropertySet ps: createPropertySets()) {			
			ps.set("v", "33");
			assertEquals("33", ps.get("v", "77"));
			assertEquals("77", ps.get("v2", "77"));
		}
	}

	@Test
	public void testGetConvertedDefaultValue() {
		for (LocalStringPropertySet ps: createPropertySets()) {			
			ps.set("v", "33");
			assertEquals(33, (Object) ps.get("v", Integer.class, 77));
			assertEquals(77, (Object) ps.get("v2", Integer.class, 77));
		}
	}

	@Test
	public void testCookCallable() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			pss[i].cook("v", new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			});
			
			Thread.sleep(500);
			if (i>0) {
				assertTrue(called[0]);
			}
			
			assertEquals("Hello", pss[i].get("v"));
		}
	}

	@Test
	public void testDelayedCook() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			pss[i].cook("v", new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			}, 100, TimeUnit.MILLISECONDS);
			
			Thread.sleep(50);
			if (i>1) {
				assertFalse(called[0]);
			}
			Thread.sleep(200);
			if (i>0) {
				assertTrue(called[0]);
			}
			
			assertEquals("Hello", pss[i].get("v"));
		}
	}

	@Test
	public void testCooking() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			Future<String> future = executorService.submit(new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			});
			
			pss[i].cooking("v", future);
			
			Thread.sleep(500);
			assertTrue(called[0]);
			assertEquals("Hello", pss[i].get("v"));
		}
	}

	@Test
	public void testLazy() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			pss[i].lazy("v", new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			});
			
			Thread.sleep(500);
			assertFalse(called[0]);
			assertEquals("Hello", pss[i].get("v"));
			assertTrue(called[0]);
		}
	}

	@Test
	public void testCancel() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			pss[i].cook("v", new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			}, 100, TimeUnit.MILLISECONDS);
			
			pss[i].cancel("v");
			Thread.sleep(200);
			if (i!=1) {
				assertFalse(called[0]);
			}
			
			assertEquals("Hello", pss[i].get("v"));
		}
	}

	@Test
	public void testCancelAll() throws InterruptedException {
		LocalStringPropertySet[] pss = createPropertySets();
		for (int i=0; i<pss.length; ++i) {	
			final boolean[] called = new boolean[] {false};
			
			pss[i].cook("v", new Callable<String>() {

				@Override
				public String call() throws Exception {
					called[0] = true;
					return "Hello";
				}
				
			}, 100, TimeUnit.MILLISECONDS);
			
			pss[i].cancelAll();
			Thread.sleep(200);
			assertFalse(called[0]);
			
			assertEquals("Hello", pss[i].get("v"));
		}
	}

	@Test
	public void testInvoke() {
		for (LocalStringPropertySet ps: createPropertySets()) {
			
			Invocable<PropertySet<String>, Object, String, RuntimeException> invocable = new Invocable<PropertySet<String>, Object, String, RuntimeException>() {

				@Override
				public Object invoke(
						PropertySet<String> context,
						Invocation<String> invocation,
						ExecutorService executorService) {
					
					return "33"+invocation.getArguments()[0];
				}

				@Override
				public Class getReturnType() {
					return Integer.class;
				}

				@Override
				public Class<?>[] getParameterTypes() {
					return new Class[] {Integer.class};
				}
				
			};
			
			ps.setInvocable("concat", invocable);

			Object ret = ps.invoke("concat", "88");
			assertEquals("3388", ret);
		}
	}

	@Test
	public void testChain() {
		LocalStringPropertySet chain = new LocalStringPropertySet(executorService, null, null, null);
		chain.set("v", 33);
		Collection<String> multi = new ArrayList<String>();
		chain.set("multi", multi);
		multi.add("Hello");
		
		for (LocalStringPropertySet ps: createPropertySets(chain)) {
			assertEquals(33, ps.get("v"));			
			ps.set("v1", 88);
			assertEquals(88, ps.get("v1"));
			assertNull(chain.get("v1"));
			
			Collection<String> multiClone = ps.get("multi", Collection.class);
			
			assertNotSame(multi, multiClone);
			
			multiClone.add("World");
			assertEquals(1, multi.size());
			assertEquals(2, multiClone.size());			
			
		}
	}

}
