/*******************************************************************************
*   Copyright 2014 Analog Devices, Inc.
*
*   Licensed under the Apache License, Version 2.0 (the "License");
*   you may not use this file except in compliance with the License.
*   You may obtain a copy of the License at
*
*       http://www.apache.org/licenses/LICENSE-2.0
*
*   Unless required by applicable law or agreed to in writing, software
*   distributed under the License is distributed on an "AS IS" BASIS,
*   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*   See the License for the specific language governing permissions and
*   limitations under the License.
********************************************************************************/

package com.analog.lyric.options.tests;

import static com.analog.lyric.util.test.ExceptionTester.*;
import static org.junit.Assert.*;

import java.io.Serializable;

import org.eclipse.jdt.annotation.Nullable;
import org.junit.Test;

import com.analog.lyric.dimple.exceptions.DimpleException;
import com.analog.lyric.options.BooleanOptionKey;
import com.analog.lyric.options.ClassOptionKey;
import com.analog.lyric.options.DoubleListOptionKey;
import com.analog.lyric.options.DoubleOptionKey;
import com.analog.lyric.options.DoubleRangeOptionKey;
import com.analog.lyric.options.EnumOptionKey;
import com.analog.lyric.options.GenericOptionKey;
import com.analog.lyric.options.IOptionHolder;
import com.analog.lyric.options.IOptionKey;
import com.analog.lyric.options.IntegerListOptionKey;
import com.analog.lyric.options.IntegerOptionKey;
import com.analog.lyric.options.LongOptionKey;
import com.analog.lyric.options.OptionDoubleList;
import com.analog.lyric.options.OptionIntegerList;
import com.analog.lyric.options.OptionKey;
import com.analog.lyric.options.OptionKeys;
import com.analog.lyric.options.OptionStringList;
import com.analog.lyric.options.OptionValidationException;
import com.analog.lyric.options.StringListOptionKey;
import com.analog.lyric.options.StringOptionKey;
import com.analog.lyric.util.test.SerializationTester;

public class TestOptionKey
{
	public static final BooleanOptionKey YES =
		new BooleanOptionKey(TestOptionKey.class, "YES");
	
	public static final BooleanOptionKey LOCAL_YES =
		new BooleanOptionKey(TestOptionKey.class, "LOCAL_YES", false, IOptionKey.Lookup.LOCAL);
	
	public static final IOptionKey<Double> P =
		new DoubleOptionKey(TestOptionKey.class, "P");
	
	public static final IOptionKey<Double> LOCAL_P =
		new DoubleOptionKey(TestOptionKey.class, "LOCAL_P", 0.0, 0.0, 1.0, IOptionKey.Lookup.LOCAL);
	
	public static final IOptionKey<Integer> I =
		new IntegerOptionKey(TestOptionKey.class, "I");
	
	public static final IOptionKey<Integer> LOCAL_I =
		new IntegerOptionKey(TestOptionKey.class, "LOCAL_I", 0, 0, 100, IOptionKey.Lookup.LOCAL);

	public static final IOptionKey<String> S =
		new StringOptionKey(TestOptionKey.class, "S");
	
	public static final IOptionKey<String> LOCAL_S =
		new StringOptionKey(TestOptionKey.class, "LOCAL_S", "", IOptionKey.Lookup.LOCAL);
	
	public static final IOptionKey<String> G =
		new GenericOptionKey<String>(TestOptionKey.class, "G", String.class, "g");
	
	public static final StringListOptionKey SL0 =
		new StringListOptionKey(TestOptionKey.class, "SL0");
	
	public static final StringListOptionKey SL2 =
		new StringListOptionKey(TestOptionKey.class, "SL2", "a", "b");
	
	public static final DoubleListOptionKey DL0 =
		new DoubleListOptionKey(TestOptionKey.class, "DL0");
	
	public static final DoubleListOptionKey DL2 =
		new DoubleListOptionKey(TestOptionKey.class, "DL2", 2.3, 4.5);
	
	public static final DoubleRangeOptionKey UNBOUNDED =
		new DoubleRangeOptionKey(TestOptionKey.class, "UNBOUNDED");
	
	public static final DoubleRangeOptionKey UNIT =
		new DoubleRangeOptionKey(TestOptionKey.class, "UNIT", 0.0, 1.0);
	
	public static final IntegerListOptionKey IL0 =
		new IntegerListOptionKey(TestOptionKey.class, "IL0");
	
	public static final IntegerListOptionKey IL2 =
		new IntegerListOptionKey(TestOptionKey.class, "IL2", 23, 45);

	public static final IntegerOptionKey DIGIT =
		new IntegerOptionKey(TestOptionKey.class, "DIGIT", 0, 0, 9);
	
	public static final DoubleOptionKey PROB =
		new DoubleOptionKey(TestOptionKey.class, "PROB", .5, 0.0, 1.0);
	
	public static final LongOptionKey LONG_BOUNDED =
		new LongOptionKey(TestOptionKey.class, "LONG_BOUNDED", 2L, 0L, 0xFFFFFFFFFFL);
	
	public static final ClassOptionKey<CharSequence> CLASS =
		new ClassOptionKey<>(TestOptionKey.class, "CLASS", CharSequence.class, String.class);
	
	public static enum Color
	{
		RED, GREEN, BLUE;
	}
	
	public static final EnumOptionKey<Color> COLOR =
		new EnumOptionKey<Color>(TestOptionKey.class, "COLOR", Color.class, Color.RED);
	
	public static final EnumOptionKey<Color> LOCAL_COLOR =
		new EnumOptionKey<Color>(TestOptionKey.class, "LOCAL_COLOR", Color.class, Color.RED, IOptionKey.Lookup.LOCAL);
	
	@SuppressWarnings("null")
	public static enum Option implements IOptionKey<Serializable>
	{
		A(42),
		B("barf"),
		C(1.0);
		
		private final Serializable _defaultValue;
		
		private Option(Serializable defaultValue) { _defaultValue = defaultValue; }

		@Override
		public @Nullable Object convertToExternal(Serializable value)
		{
			return value;
		}

		@Override
		public Serializable convertToValue(@Nullable Object value)
		{
			return type().cast(value);
		}
		
		@Override
		public Class<Serializable> type()
		{
			return Serializable.class;
		}

		@Override
		public Serializable defaultValue()
		{
			return _defaultValue;
		}

		@Override
		public @Nullable Serializable getOrDefault(IOptionHolder holder)
		{
			return holder.getOptionOrDefault(this);
		}

		@Override
		public @Nullable Serializable get(IOptionHolder holder)
		{
			return holder.getOption(this);
		}

		@Override
		public boolean local()
		{
			return false;
		}

		@Override
		public IOptionKey.Lookup lookupMethod()
		{
			 return IOptionKey.Lookup.NONLOCAL;
		}

		 @Override
		public void set(IOptionHolder holder, Serializable value)
		{
			holder.setOption(this, value);
		}

		@Override
		public void unset(IOptionHolder holder)
		{
			holder.unsetOption(this);
		}

		@Override
		public boolean validForDelegator(Serializable value, IOptionHolder delegator)
		{
			return true;
		}
		
		@Override
		public Serializable validate(Serializable value, IOptionHolder optionHolder)
		{
			return type().cast(value);
		}
		
	}
	
	private static enum NotAnOptionKey
	{
		INSTANCE;
	}
	
	@Test
	public void test()
	{
		for (IOptionKey<?> key : OptionKeys.declaredInClass(getClass()).values())
		{
			assertOptionInvariants(key);
		}
		
		for (Option key : Option.values())
		{
			assertOptionInvariants(key);
		}
		
		// Test IntegerOptionKey
		assertEquals((Integer)3, DIGIT.validate(3, null));
		assertEquals((Integer)0, DIGIT.validate(0, null));
		assertEquals((Integer)9, DIGIT.validate(9, null));
		expectThrow(OptionValidationException.class, DIGIT, "validate", -1, null);
		expectThrow(OptionValidationException.class, DIGIT, "validate", 10, null);
		assertEquals(0, DIGIT.lowerBound());
		assertEquals(9, DIGIT.upperBound());
		assertEquals((Integer)42, I.convertToValue(42.0));
		try
		{
			I.convertToValue(42.5);
			fail("exception expected");
		}
		catch (IllegalArgumentException ex)
		{
		}
		try
		{
			I.convertToValue("42.5");
			fail("exception expected");
		}
		catch (ClassCastException ex)
		{
		}
		
		// Test LongOptionKey
		assertEquals(0L, LONG_BOUNDED.lowerBound());
		assertEquals(0xFFFFFFFFFFL, LONG_BOUNDED.upperBound());
		assertEquals((Long)LONG_BOUNDED.lowerBound(), LONG_BOUNDED.validate(LONG_BOUNDED.lowerBound(), null));
		assertEquals((Long)LONG_BOUNDED.upperBound(), LONG_BOUNDED.validate(LONG_BOUNDED.upperBound(), null));
		expectThrow(OptionValidationException.class, LONG_BOUNDED, "validate", LONG_BOUNDED.lowerBound() - 1, null);
		expectThrow(OptionValidationException.class, LONG_BOUNDED, "validate", LONG_BOUNDED.upperBound() + 1, null);
		assertEquals((Long)23L, LONG_BOUNDED.convertToValue(23.0));
		expectThrow(IllegalArgumentException.class, LONG_BOUNDED, "convertToValue", (Object)23.4);
		
		// Test DoubleOptionKey
		assertEquals(0.0, PROB.validate(0.0, null), 0.0);
		assertEquals(1.0, PROB.validate(1.0, null), 0.0);
		assertEquals(.3, PROB.validate(.3, null), 0.0);
		expectThrow(OptionValidationException.class, PROB, "validate", -0.0001, null);
		expectThrow(OptionValidationException.class, PROB, "validate", 1.0001, null);
		expectThrow(OptionValidationException.class, PROB, "validate", Double.NaN, null);
		assertEquals(0.0, PROB.lowerBound(), 0.0);
		assertEquals(1.0, PROB.upperBound(), 0.0);
		
		// Test EnumOptionKey
		assertEquals(Color.RED, COLOR.defaultValue());
		assertEquals(Color.RED, COLOR.convertToValue("RED"));
		assertEquals(Color.BLUE, COLOR.convertToValue("BLUE"));
		expectThrow(IllegalArgumentException.class, COLOR, "convertToValue", "gag");
		expectThrow(IllegalArgumentException.class, COLOR, "convertToValue", "blue"); // case-sensitive
		
		// Test ClassOptionKey
		assertEquals(String.class, CLASS.defaultValue());
		assertEquals(CharSequence.class, CLASS.superClass());
		assertEquals(String.class, CLASS.convertToValue("java.lang.String"));
		expectThrow(ClassCastException.class, CLASS, "convertToValue", new int[] {2});
		assertSame(Object.class, CLASS.convertToValue(Object.class)); // Does not validate
		expectThrow(OptionValidationException.class, "Could not construct class.*", CLASS, "convertToValue", "bogus");
		expectThrow(OptionValidationException.class, ".*is not a subclass.*", CLASS, "validate", Object.class, null);
		
		// Test list keys
//		ExampleOptionHolder holder = new ExampleOptionHolder();
		assertTrue(SL0.defaultValue().isEmpty());
		assertArrayEquals(new Object[] { "a", "b" }, SL2.defaultValue().toArray());
		
		
		OptionKey<String> rogueKey = new StringOptionKey(Object.class, "foo");
		assertNull(OptionKey.getCanonicalInstance(rogueKey));
		
		OptionKey<String> rogueKey2 = SerializationTester.clone(rogueKey);
		assertNotSame(rogueKey2, rogueKey);
		assertSame(rogueKey.getDeclaringClass(), rogueKey2.getDeclaringClass());
		assertEquals(rogueKey.name(), rogueKey2.name());
		assertEquals(rogueKey.defaultValue(), rogueKey2.defaultValue());
		
		// DoubleRangeOptionKey
		expectThrow(OptionValidationException.class, "Expected valid double range.*", UNBOUNDED, "validate",
			new OptionDoubleList(1.0), null);
		expectThrow(OptionValidationException.class, "Expected valid double range.*", UNBOUNDED, "validate",
			new OptionDoubleList(1.0, 0.0), null);
		
		//
		// Error cases
		//
		
		expectThrow(DimpleException.class, "Error loading option key 'INSTANCE'.*",
			OptionKey.class, "inClass",	NotAnOptionKey.class, "INSTANCE");
		
		expectThrow(DimpleException.class, "Error loading option key 'NOT_AN_OPTION'.*",
			OptionKey.class, "inClass",	Option.class, "NOT_AN_OPTION");
		
		expectThrow(DimpleException.class, "'frob' is not a canonical option key name",
			OptionKey.class, "forCanonicalName", "frob");
		
		expectThrow(DimpleException.class, ".*ClassNotFoundException.*",
			OptionKey.class, "forCanonicalName", "no.such.package.NoSuchClass");
	}

	<T extends Serializable> void assertOptionInvariants(IOptionKey<T> key)
	{
		Class<? extends T> type = key.type();
		Class<?> declaringClass = key.getDeclaringClass();
		assertEquals(key.local(), key.lookupMethod()== IOptionKey.Lookup.LOCAL);
		assertEquals(key.local(), key.name().startsWith("LOCAL_"));
		assertNotNull(key.name());
		assertNotNull(type);
		assertTrue(type.isInstance(key.defaultValue()));
		assertEquals(key.defaultValue(), key.convertToValue(key.defaultValue()));
		assertEquals(key.defaultValue(), key.convertToValue(key.convertToExternal(key.defaultValue())));
		assertEquals(key.defaultValue(), key.validate(key.defaultValue(), null));
		if (declaringClass.isEnum())
		{
			assertEquals(key.name(), key.toString());
		}
		else
		{
			assertEquals(OptionKey.qualifiedName(key), key.toString());
		}
		
		IOptionKey<?> key3 = OptionKey.inClass(key.getDeclaringClass(), key.name());
		assertSame(key3, key);
		
		IOptionKey<?> key4 = OptionKey.forCanonicalName(OptionKey.canonicalName(key));
		assertSame(key, key4);
		
		IOptionKey<?> key2 = SerializationTester.clone(key);
		assertSame(key2, key);
		
		assertEquals(declaringClass.getName() + "." + key.name(), OptionKey.canonicalName(key));
		assertEquals(declaringClass.getSimpleName() + "." + key.name(), OptionKey.qualifiedName(key));
		if (key instanceof OptionKey)
		{
			assertEquals(OptionKey.canonicalName(key), ((OptionKey<?>)key).canonicalName());
			assertEquals(OptionKey.qualifiedName(key), ((OptionKey<?>)key).qualifiedName());
		}
		
		
		ExampleOptionHolder holder = new ExampleOptionHolder();
		assertEquals(key.defaultValue(), key.getOrDefault(holder));
		
		for (Object newValue : new Object[]
			{ "foo", 7, .314159, new OptionStringList("foo", "bar"), new OptionDoubleList(1.324, 234234.2),
			new OptionIntegerList(2,3,4,5)}
		)
		{
			if (type.isInstance(newValue))
			{
				key.set(holder, type.cast(newValue));
				assertEquals(newValue, key.get(holder));
				assertEquals(newValue, key.getOrDefault(holder));
				key.unset(holder);
				assertNull(key.get(holder));
				assertEquals(key.defaultValue(), key.getOrDefault(holder));
				if (key instanceof OptionKey)
				{
					((OptionKey<?>)key).convertAndSet(holder, newValue);
					assertEquals(newValue, key.get(holder));
					assertEquals(newValue, key.getOrDefault(holder));
					key.unset(holder);
				}
			}
		}
	}
	
}
