/*******************************************************************************
 * Copyright (c) 2006, 2009 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
 *     Matthew Hall - bugs 213145, 241585, 246103, 194734, 268688
 *     Ovidio Mallo - bug 247741
 *******************************************************************************/

package org.eclipse.core.tests.internal.databinding.beans;

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

import java.beans.PropertyDescriptor;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;

import org.eclipse.core.databinding.beans.typed.BeanProperties;
import org.eclipse.core.databinding.beans.BeansObservables;
import org.eclipse.core.databinding.beans.IBeanObservable;
import org.eclipse.core.databinding.beans.IBeanProperty;
import org.eclipse.core.databinding.beans.PojoObservables;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.map.MapDiff;
import org.eclipse.core.databinding.observable.set.WritableSet;
import org.eclipse.core.tests.databinding.observable.ThreadRealm;
import org.eclipse.jface.databinding.conformance.util.ChangeEventTracker;
import org.eclipse.jface.databinding.conformance.util.CurrentRealm;
import org.eclipse.jface.databinding.conformance.util.MapChangeEventTracker;
import org.eclipse.jface.tests.databinding.AbstractDefaultRealmTestCase;
import org.junit.Before;
import org.junit.Test;

/**
 * @since 3.2
 *
 */
public class JavaBeanObservableMapTest extends AbstractDefaultRealmTestCase {
	private Bean model1;

	private Bean model2;

	private WritableSet set;

	private PropertyDescriptor propertyDescriptor;

	private IObservableMap map;
	private IBeanObservable beanObservable;

	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();

		ThreadRealm realm = new ThreadRealm();
		realm.init(Thread.currentThread());
		model1 = new Bean("1");
		model2 = new Bean("2");

		set = new WritableSet(realm, new HashSet(), Bean.class);
		set.add(model1);
		set.add(model2);

		String propertyName = "value";
		propertyDescriptor = ((IBeanProperty) BeanProperties.value(Bean.class,
				propertyName)).getPropertyDescriptor();
		map = BeansObservables.observeMap(set, Bean.class, propertyName);
		beanObservable = (IBeanObservable) map;
	}

	@Test
	public void testGetValue() throws Exception {
		assertEquals(
				"The 'value' from the map should be the value of the property of the model.",
				model1.getValue(), map.get(model1));
	}

	@Test
	public void testGetValue_KeyOutOfDomain() {
		Bean model3 = new Bean("3");
		assertFalse(map.containsKey(model3));
		assertFalse(model3.getValue().equals(map.get(model3)));
	}

	@Test
	public void testSetValueNotifications() throws Exception {
		String oldValue = model1.getValue();
		String newValue = model1.getValue() + model1.getValue();
		MapChangeEventTracker listener = new MapChangeEventTracker();

		map.addMapChangeListener(listener);
		assertEquals(0, listener.count);
		model1.setValue(newValue);
		assertEquals(1, listener.count);
		assertTrue(listener.event.diff.getChangedKeys().contains(model1));
		assertEquals(newValue, listener.event.diff.getNewValue(model1));
		assertEquals(oldValue, listener.event.diff.getOldValue(model1));
		assertFalse(listener.event.diff.getAddedKeys().contains(model1));
		assertFalse(listener.event.diff.getRemovedKeys().contains(model1));
	}

	@Test
	public void testPutValue() throws Exception {
		String oldValue = model1.getValue();
		String newValue = model1.getValue() + model1.getValue();
		MapChangeEventTracker listener = new MapChangeEventTracker();
		map.addMapChangeListener(listener);

		assertEquals(0, listener.count);
		map.put(model1, newValue);
		assertEquals(1, listener.count);
		assertEquals(newValue, model1.getValue());
		assertEquals(oldValue, listener.event.diff.getOldValue(model1));
		assertEquals(newValue, listener.event.diff.getNewValue(model1));
		assertFalse(listener.event.diff.getAddedKeys().contains(model1));
		assertTrue(listener.event.diff.getChangedKeys().contains(model1));
		assertFalse(listener.event.diff.getRemovedKeys().contains(model1));
	}

	@Test
	public void testAddKey() throws Exception {
		MapChangeEventTracker listener = new MapChangeEventTracker();
		map.addMapChangeListener(listener);

		Bean model3 = new Bean("3");

		assertEquals(0, listener.count);
		set.add(model3);
		assertEquals(1, listener.count);
		assertTrue(listener.event.diff.getAddedKeys().contains(model3));
		assertEquals(model3.getValue(), map.get(model3));

		String newValue = model3.getValue() + model3.getValue();
		model3.setValue(newValue);
		assertEquals(2, listener.count);
		assertEquals(3, map.size());
	}

	@Test
	public void testRemoveKey() throws Exception {
		MapChangeEventTracker listener = new MapChangeEventTracker();
		map.addMapChangeListener(listener);

		assertEquals(0, listener.count);
		set.remove(model1);
		assertEquals(1, listener.count);
		assertFalse(listener.event.diff.getAddedKeys().contains(model1));
		assertFalse(listener.event.diff.getChangedKeys().contains(model1));
		assertTrue(listener.event.diff.getRemovedKeys().contains(model1));
		assertEquals(1, map.size());
	}

	@Test
	public void testGetObserved() throws Exception {
		assertEquals(set, beanObservable.getObserved());
	}

	@Test
	public void testGetPropertyDescriptor() throws Exception {
		assertEquals(propertyDescriptor, beanObservable.getPropertyDescriptor());
	}

	@Test
	public void testConstructor_SkipRegisterListeners() throws Exception {
		Realm realm = new CurrentRealm(true);
		WritableSet set = new WritableSet(realm);
		Bean bean = new Bean();
		set.add(bean);

		IObservableMap observable = PojoObservables.observeMap(set, Bean.class,
				"value");
		assertFalse(bean.hasListeners("value"));
		ChangeEventTracker.observe(observable);

		assertFalse(bean.hasListeners("value"));
	}

	@Test
	public void testConstructor_RegistersListeners() throws Exception {
		Realm realm = new CurrentRealm(true);
		WritableSet set = new WritableSet(realm);
		Bean bean = new Bean();
		set.add(bean);

		IObservableMap observable = BeansObservables.observeMap(set,
				Bean.class, "value");
		assertFalse(bean.hasListeners("value"));
		ChangeEventTracker.observe(observable);

		assertTrue(bean.hasListeners("value"));
	}

	@Test
	public void testSetBeanProperty_CorrectForNullOldAndNewValues() {
		// The java bean spec allows the old and new values in a
		// PropertyChangeEvent to be null, which indicates that an unknown
		// change occured.

		// This test ensures that JavaBeanObservableValue fires the correct
		// value diff even if the bean implementor is lazy :-P

		WritableSet set = new WritableSet(new CurrentRealm(true));

		Bean bean = new AnnoyingBean();
		bean.setValue("old");
		set.add(bean);

		IObservableMap map = BeansObservables.observeMap(set, Bean.class,
				"value");
		MapChangeEventTracker tracker = MapChangeEventTracker.observe(map);

		bean.setValue("new");

		assertEquals(1, tracker.count);

		assertEquals(Collections.EMPTY_SET, tracker.event.diff.getAddedKeys());
		assertEquals(Collections.singleton(bean), tracker.event.diff
				.getChangedKeys());
		assertEquals(Collections.EMPTY_SET, tracker.event.diff.getRemovedKeys());

		assertEquals("old", tracker.event.diff.getOldValue(bean));
		assertEquals("new", tracker.event.diff.getNewValue(bean));
	}

	@Test
	public void testModifyObservableMap_FiresMapChange() {
		Bean bean = new Bean(Collections.singletonMap("key", "oldValue"));
		IObservableMap observable = BeansObservables.observeMap(bean, "map");
		MapChangeEventTracker tracker = MapChangeEventTracker
				.observe(observable);

		observable.put("key", "newValue");

		assertEquals(1, tracker.count);
		assertDiff(tracker.event.diff, Collections.singletonMap("key",
				"oldValue"), Collections.singletonMap("key", "newValue"));
	}

	@Test
	public void testSetBeanPropertyOutsideRealm_FiresEventInsideRealm() {
		Bean bean = new Bean(Collections.EMPTY_MAP);
		CurrentRealm realm = new CurrentRealm(true);
		IObservableMap observable = BeansObservables.observeMap(realm, bean,
				"map");
		MapChangeEventTracker tracker = MapChangeEventTracker
				.observe(observable);

		realm.setCurrent(false);
		bean.setMap(Collections.singletonMap("key", "value"));
		assertEquals(0, tracker.count);

		realm.setCurrent(true);
		assertEquals(1, tracker.count);
		assertDiff(tracker.event.diff, Collections.EMPTY_MAP, Collections
				.singletonMap("key", "value"));
	}

	private static void assertDiff(MapDiff diff, Map oldMap, Map newMap) {
		oldMap = new HashMap(oldMap); // defensive copy in case arg is
		// unmodifiable
		diff.applyTo(oldMap);
		assertEquals("applying diff to list did not produce expected result",
				newMap, oldMap);
	}
}
