/*******************************************************************************
 * Copyright (c) 2008, 2017 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.osgi.tests.filter;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import junit.framework.AssertionFailedError;
import org.eclipse.osgi.framework.util.CaseInsensitiveDictionaryMap;
import org.eclipse.osgi.tests.util.MapDictionary;
import org.junit.Assert;
import org.junit.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.Filter;
import org.osgi.framework.InvalidSyntaxException;
import org.osgi.framework.ServiceReference;

public abstract class FilterTests {

	/**
	 * Fail with cause t.
	 *
	 * @param message Failure message.
	 * @param t       Cause of the failure.
	 */
	public static void fail(String message, Throwable t) {
		AssertionFailedError e = new AssertionFailedError(message + ": " + t.getMessage());
		e.initCause(t);
		throw e;
	}

	static final int ISTRUE = 1;
	static final int ISFALSE = 2;
	static final int ISILLEGAL = 3;

	public abstract Filter createFilter(String filterString) throws InvalidSyntaxException;

	private Dictionary getProperties() {
		Dictionary props = new Hashtable();
		props.put("room", "bedroom");
		props.put("channel", new Object[] { Integer.valueOf(34), "101" });
		props.put("status", "(on\\)*");
		List vec = new ArrayList(10);
		vec.add(Long.valueOf(150));
		vec.add("100");
		props.put("max record time", vec);
		props.put("canrecord", "true(x)");
		props.put("shortvalue", Short.valueOf((short) 1000));
		props.put("intvalue", Integer.valueOf(100000));
		props.put("longvalue", Long.valueOf(10000000000L));
		props.put("bytevalue", Byte.valueOf((byte) 10));
		props.put("floatvalue", Float.valueOf(1.01f));
		props.put("doublevalue", Double.valueOf(2.01));
		props.put("charvalue", Character.valueOf('A'));
		props.put("booleanvalue", Boolean.TRUE);
		props.put("weirdvalue", new Hashtable());
		props.put("primintarrayvalue", new int[] { 1, 2, 3 });
		props.put("primlongarrayvalue", new long[] { 1, 2, 3 });
		props.put("primbytearrayvalue", new byte[] { (byte) 1, (byte) 2, (byte) 3 });
		props.put("primshortarrayvalue", new short[] { (short) 1, (short) 2, (short) 3 });
		props.put("primfloatarrayvalue", new float[] { (float) 1.1, (float) 2.2, (float) 3.3 });
		props.put("primdoublearrayvalue", new double[] { 1.1, 2.2, 3.3 });
		props.put("primchararrayvalue", new char[] { 'A', 'b', 'C', 'd' });
		props.put("primbooleanarrayvalue", new boolean[] { false });
		props.put("bigintvalue", new BigInteger("4123456"));
		props.put("bigdecvalue", new BigDecimal("4.123456"));
		props.put("*", "foo");
		props.put("!  ab", "b");
		props.put("|   ab", "b");
		props.put("&    ab", "b");
		props.put("!", "c");
		props.put("|", "c");
		props.put("&", "c");
		props.put("empty", "");
		props.put("space", Character.valueOf(' '));
		return props;
	}

	@Test
	public void testFilter() {
		Dictionary props = getProperties();
		testFilter("(room=*)", props, ISTRUE);
		testFilter("(room=bedroom)", props, ISTRUE);
		testFilter("(room~= B E D R O O M )", props, ISTRUE);
		testFilter("(room=abc)", props, ISFALSE);
		testFilter(" ( room >=aaaa)", props, ISTRUE);
		testFilter("(room <=aaaa)", props, ISFALSE);
		testFilter("  ( room =b*) ", props, ISTRUE);
		testFilter("  ( room =*m) ", props, ISTRUE);
		testFilter("(room=bed*room)", props, ISTRUE);
		testFilter("  ( room =b*oo*m) ", props, ISTRUE);
		testFilter("  ( room =*b*oo*m*) ", props, ISTRUE);
		testFilter("  ( room =b*b*  *m*) ", props, ISFALSE);
		testFilter("  (& (room =bedroom) (channel ~=34))", props, ISTRUE);
		testFilter("  (&  (room =b*)  (room =*x) (channel=34))", props, ISFALSE);
		testFilter("(| (room =bed*)(channel=222)) ", props, ISTRUE);
		testFilter("(| (room =boom*)(channel=101)) ", props, ISTRUE);
		testFilter("  (! (room =ab*b*oo*m*) ) ", props, ISTRUE);
		testFilter("  (status =\\(o*\\\\\\)\\*) ", props, ISTRUE);
		testFilter("  (canRecord =true\\(x\\)) ", props, ISTRUE);
		testFilter("(max Record Time <=140) ", props, ISTRUE);
		testFilter("(shortValue >=100) ", props, ISTRUE);
		testFilter("(intValue <=100001) ", props, ISTRUE);
		testFilter("(longValue >=10000000000) ", props, ISTRUE);
		testFilter("  (  &  (  byteValue <=100)  (  byteValue >=10)  )  ", props, ISTRUE);
		testFilter("(weirdValue =100) ", props, ISFALSE);
		testFilter("(bigIntValue =4123456) ", props, ISTRUE);
		testFilter("(bigDecValue =4.123456) ", props, ISTRUE);
		testFilter("(floatValue >=1.0) ", props, ISTRUE);
		testFilter("(doubleValue <=2.011) ", props, ISTRUE);
		testFilter("(charValue ~=a) ", props, ISTRUE);
		testFilter("(booleanValue =true) ", props, ISTRUE);
		testFilter("(primIntArrayValue =1) ", props, ISTRUE);
		testFilter("(primLongArrayValue =2) ", props, ISTRUE);
		testFilter("(primByteArrayValue =3) ", props, ISTRUE);
		testFilter("(primShortArrayValue =1) ", props, ISTRUE);
		testFilter("(primFloatArrayValue =1.1) ", props, ISTRUE);
		testFilter("(primDoubleArrayValue =2.2) ", props, ISTRUE);
		testFilter("(primCharArrayValue ~=D) ", props, ISTRUE);
		testFilter("(primBooleanArrayValue =false) ", props, ISTRUE);
		testFilter("(& (| (room =d*m) (room =bed*) (room=abc)) (! (channel=999)))", props, ISTRUE);
		testFilter("(room=bedroom)", null, ISFALSE);
		testFilter("(*=foo)", props, ISTRUE);
		testFilter("(!  ab=b)", props, ISTRUE);
		testFilter("(|   ab=b)", props, ISTRUE);
		testFilter("(&=c)", props, ISTRUE);
		testFilter("(!=c)", props, ISTRUE);
		testFilter("(|=c)", props, ISTRUE);
		testFilter("(&    ab=b)", props, ISTRUE);
		testFilter("(!ab=*)", props, ISFALSE);
		testFilter("(|ab=*)", props, ISFALSE);
		testFilter("(&ab=*)", props, ISFALSE);
		testFilter("(empty=)", props, ISTRUE);
		testFilter("(empty=*)", props, ISTRUE);
		testFilter("(space= )", props, ISTRUE);
		testFilter("(space=*)", props, ISTRUE);
	}

	@Test
	public void testInvalidValues() {
		Dictionary props = getProperties();
		testFilter("(intvalue=*)", props, ISTRUE);
		testFilter("(intvalue=b)", props, ISFALSE);
		testFilter("(intvalue=)", props, ISFALSE);
		testFilter("(longvalue=*)", props, ISTRUE);
		testFilter("(longvalue=b)", props, ISFALSE);
		testFilter("(longvalue=)", props, ISFALSE);
		testFilter("(shortvalue=*)", props, ISTRUE);
		testFilter("(shortvalue=b)", props, ISFALSE);
		testFilter("(shortvalue=)", props, ISFALSE);
		testFilter("(bytevalue=*)", props, ISTRUE);
		testFilter("(bytevalue=b)", props, ISFALSE);
		testFilter("(bytevalue=)", props, ISFALSE);
		testFilter("(charvalue=*)", props, ISTRUE);
		testFilter("(charvalue=)", props, ISFALSE);
		testFilter("(floatvalue=*)", props, ISTRUE);
		testFilter("(floatvalue=b)", props, ISFALSE);
		testFilter("(floatvalue=)", props, ISFALSE);
		testFilter("(doublevalue=*)", props, ISTRUE);
		testFilter("(doublevalue=b)", props, ISFALSE);
		testFilter("(doublevalue=)", props, ISFALSE);
		testFilter("(booleanvalue=*)", props, ISTRUE);
		testFilter("(booleanvalue=b)", props, ISFALSE);
		testFilter("(booleanvalue=)", props, ISFALSE);
	}

	@Test
	public void testIllegal() {
		Dictionary props = getProperties();
		testFilter("", props, ISILLEGAL);
		testFilter("()", props, ISILLEGAL);
		testFilter("(=foo)", props, ISILLEGAL);
		testFilter("(", props, ISILLEGAL);
		testFilter("(abc = ))", props, ISILLEGAL);
		testFilter("(& (abc = xyz) (& (345))", props, ISILLEGAL);
		testFilter("  (room = b**oo!*m*) ) ", props, ISILLEGAL);
		testFilter("  (room = b**oo)*m*) ) ", props, ISILLEGAL);
		testFilter("  (room = *=b**oo*m*) ) ", props, ISILLEGAL);
		testFilter("  (room = =b**oo*m*) ) ", props, ISILLEGAL);
	}

	@Test
	public void testScalarSubstring() {
		Dictionary props = getProperties();
		testFilter("(shortValue =100*) ", props, ISFALSE);
		testFilter("(intValue =100*) ", props, ISFALSE);
		testFilter("(longValue =100*) ", props, ISFALSE);
		testFilter("(  byteValue =1*00  )", props, ISFALSE);
		testFilter("(bigIntValue =4*23456) ", props, ISFALSE);
		testFilter("(bigDecValue =4*123456) ", props, ISFALSE);
		testFilter("(floatValue =1*0) ", props, ISFALSE);
		testFilter("(doubleValue =2*011) ", props, ISFALSE);
		testFilter("(charValue =a*) ", props, ISFALSE);
		testFilter("(booleanValue =t*ue) ", props, ISFALSE);
	}

	@Test
	public void testNormalization() {
		try {
			Filter f1 = createFilter("( a = bedroom  )");
			Filter f2 = createFilter(" (a= bedroom  ) ");
			assertEquals("not equal", "(a= bedroom  )", f1.toString());
			assertEquals("not equal", "(a= bedroom  )", f2.toString());
			assertEquals("not equal", f1, f2);
			assertEquals("not equal", f2, f1);
			assertEquals("not equal", f1.hashCode(), f2.hashCode());
		} catch (InvalidSyntaxException e) {
			fail("unexpected invalid syntax", e);
		}

	}

	private void testFilter(String query, Dictionary props, int expect) {
		final ServiceReference ref = new DictionaryServiceReference(props);
		Filter f1;
		try {
			f1 = createFilter(query);

			if (expect == ISILLEGAL) {
				Assert.fail("expected exception");
			}
		} catch (InvalidSyntaxException e) {
			if (expect != ISILLEGAL) {
				fail("exception", e);
			}
			return;
		}

		boolean val = f1.match(props);
		assertEquals("wrong result", expect == ISTRUE, val);

		val = f1.match(ref);
		assertEquals("wrong result", expect == ISTRUE, val);

		String normalized = f1.toString();
		Filter f2;
		try {
			f2 = createFilter(normalized);
		} catch (InvalidSyntaxException e) {
			fail("exception", e);
			return;
		}

		val = f2.match(props);
		assertEquals("wrong result", expect == ISTRUE, val);

		val = f2.match(ref);
		assertEquals("wrong result", expect == ISTRUE, val);

		assertEquals("normalized not equal", normalized, f2.toString());

	}

	@Test
	public void testComparable() {
		Filter f1 = null;
		Object comp42 = new SampleComparable("42");
		Object comp43 = new SampleComparable("43");
		Hashtable hash = new Hashtable();

		try {
			f1 = createFilter("(comparable=42)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("comparable", comp42);
		assertTrue("does not match filter", f1.match(hash));
		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));

		hash.put("comparable", comp43);
		assertFalse("does match filter", f1.match(hash));
		assertFalse("does match filter", f1.match(new DictionaryServiceReference(hash)));

		try {
			f1 = createFilter("(comparable<=42)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("comparable", comp42);
		assertTrue("does not match filter", f1.match(hash));
		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));

		hash.put("comparable", comp43);
		assertFalse("does match filter", f1.match(hash));
		assertFalse("does match filter", f1.match(new DictionaryServiceReference(hash)));

		try {
			f1 = createFilter("(comparable>=42)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("comparable", comp42);
		assertTrue("does not match filter", f1.match(hash));
		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));

		hash.put("comparable", comp43);
		assertTrue("does not match filter", f1.match(hash));
		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));

		try {
			f1 = createFilter("(comparable=4*2)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("comparable", comp42);
		assertFalse("does match filter", f1.match(hash));
		assertFalse("does match filter", f1.match(new DictionaryServiceReference(hash)));
	}

	@Test
	public void testObject() {
		Filter f1 = null;
		Object obj42 = new SampleObject("42");
		Object obj43 = new SampleObject("43");
		Hashtable hash = new Hashtable();

		try {
			f1 = createFilter("(object=42)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("object", obj42);
		assertTrue("does not match filter", f1.match(hash));
		assertTrue("does not match filter", f1.match(new DictionaryServiceReference(hash)));

		hash.put("object", obj43);
		assertFalse("does match filter", f1.match(hash));
		assertFalse("does match filter", f1.match(new DictionaryServiceReference(hash)));

		try {
			f1 = createFilter("(object=4*2)");
		} catch (InvalidSyntaxException e) {
			fail("invalid syntax", e);
		}

		hash.put("object", obj42);
		assertFalse("does match filter", f1.match(hash));
		assertFalse("does match filter", f1.match(new DictionaryServiceReference(hash)));
	}

	@Test
	public void testNullValueMatch() throws InvalidSyntaxException {
		Dictionary<String, Object> nullProps = new MapDictionary<>();
		nullProps.put("test.null", null);
		nullProps.put("test.non.null", "v1");
		assertFalse(createFilter("(test.null=*)").match(nullProps));
		assertTrue(createFilter("(&(!(test.null=*))(test.non.null=v1))").match(nullProps));
	}

	@Test
	public void testNullKeyMatch() throws InvalidSyntaxException {
		Dictionary<String, Object> nullProps = new MapDictionary<>();
		nullProps.put(null, "null.v1");
		nullProps.put("test.non.null", "v1");
		assertTrue(createFilter("(test.non.null=v1)").match(nullProps));
	}

	public static class SampleComparable implements Comparable {
		private int value = -1;

		public SampleComparable(String value) {
			this.value = Integer.parseInt(value);
		}

		@Override
		public int compareTo(Object o) {
			return value - ((SampleComparable) o).value;
		}

		@Override
		public String toString() {
			return String.valueOf(value);
		}
	}

	public static class SampleObject {
		private int value = -1;

		public SampleObject(String value) {
			this.value = Integer.parseInt(value);
		}

		@Override
		public boolean equals(Object o) {
			if (o instanceof SampleObject) {
				return value == ((SampleObject) o).value;
			}
			return false;
		}

		@Override
		public String toString() {
			return String.valueOf(value);
		}
	}

	private static class DictionaryServiceReference implements ServiceReference {
		private final Dictionary dictionary;
		private final String[] keys;

		DictionaryServiceReference(Dictionary dictionary) {
			if (dictionary == null) {
				this.dictionary = null;
				this.keys = new String[] {};
				return;
			}
			this.dictionary = dictionary;
			List keyList = new ArrayList(dictionary.size());
			for (Enumeration e = dictionary.keys(); e.hasMoreElements();) {
				Object k = e.nextElement();
				if (k instanceof String) {
					String key = (String) k;
					for (Iterator i = keyList.iterator(); i.hasNext();) {
						if (key.equalsIgnoreCase((String) i.next())) {
							throw new IllegalArgumentException();
						}
					}
					keyList.add(key);
				}
			}
			this.keys = (String[]) keyList.toArray(new String[keyList.size()]);
		}

		public Object getProperty(String k) {
			for (int i = 0, length = keys.length; i < length; i++) {
				String key = keys[i];
				if (key.equalsIgnoreCase(k)) {
					return dictionary.get(key);
				}
			}
			return null;
		}

		public String[] getPropertyKeys() {
			return keys.clone();
		}

		public int compareTo(Object reference) {
			throw new UnsupportedOperationException();
		}

		public Bundle getBundle() {
			throw new UnsupportedOperationException();
		}

		public Bundle[] getUsingBundles() {
			throw new UnsupportedOperationException();
		}

		public boolean isAssignableTo(Bundle bundle, String className) {
			throw new UnsupportedOperationException();
		}

		@Override
		public Dictionary getProperties() {
			if (dictionary == null) {
				return new CaseInsensitiveDictionaryMap();
			}
			return new CaseInsensitiveDictionaryMap(dictionary);
		}

		@Override
		public Object adapt(Class type) {
			return null;
		}
	}
}
