/**
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
 *
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
 * graphic logo is a trademark of OpenMRS Inc.
 */
package org.openmrs;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNot.not;
import static org.hamcrest.core.IsNull.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.apache.commons.beanutils.BeanUtils;
import org.junit.jupiter.api.Test;
import org.openmrs.api.APIException;
import org.openmrs.obs.ComplexData;
import org.openmrs.util.Reflect;

/**
 * This class tests all methods that are not getter or setters in the Obs java object TODO: finish
 * this test class for Obs
 * 
 * @see Obs
 */
public class ObsTest {
	
	private static final String VERO = "Vero";
	
	private static final String FORM_NAMESPACE_PATH_SEPARATOR = "^";
	
	//ignore these fields, groupMembers and formNamespaceAndPath field are taken care of by other tests
	private static final List<String> IGNORED_FIELDS = Arrays.asList("dirty", "log", "serialVersionUID",
	    "DATE_TIME_PATTERN", "TIME_PATTERN", "DATE_PATTERN", "FORM_NAMESPACE_PATH_SEPARATOR",
	    "FORM_NAMESPACE_PATH_MAX_LENGTH", "obsId", "groupMembers", "uuid", "changedBy", "dateChanged", "voided", "voidedBy",
	    "voidReason", "dateVoided", "formNamespaceAndPath", "$jacocoData", "referenceRange");
	
	private void resetObs(Obs obs) throws Exception {
		Field field = Obs.class.getDeclaredField("dirty");
		field.setAccessible(true);
		try {
			field.set(obs, false);
		}
		finally {
			field.setAccessible(false);
		}
		assertFalse(obs.isDirty());
	}
	
	private Obs createObs(Integer id) throws Exception {
		Obs obs = new Obs(id);
		List<Field> fields = Reflect.getAllFields(Obs.class);
		for (Field field : fields) {
			if (IGNORED_FIELDS.contains(field.getName())) {
				continue;
			}
			setFieldValue(obs, field, false);
		}
		assertFalse(obs.isDirty());
		return obs;
	}

	private void setFieldValue(Obs obs, Field field, boolean setAlternateValue) throws Exception {
		final boolean accessible = field.isAccessible();
		if (!accessible) {
			field.setAccessible(true);
		}
		try {
			Object oldFieldValue = field.get(obs);
			Object newFieldValue = generateValue(field, setAlternateValue);
			//sanity check
			if (setAlternateValue) {
				assertNotEquals(oldFieldValue,
					newFieldValue, "The old and new values should be different for field: Obs." + field.getName());
			}
			
			field.set(obs, newFieldValue);
		}
		finally {
			field.setAccessible(accessible);
		}
	}
	
	private Object generateValue(Field field, boolean setAlternateValue) throws Exception {
		Object fieldValue;
		if (field.getType().equals(Boolean.class)) {
			fieldValue = setAlternateValue;
		} else if (field.getType().equals(Integer.class)) {
			fieldValue = setAlternateValue ? 10 : 17;
		} else if (field.getType().equals(Double.class)) {
			fieldValue = setAlternateValue ? 5.0 : 7.0;
		} else if (field.getType().equals(Date.class)) {
			fieldValue = new Date();
			if (setAlternateValue) {
				Calendar c = Calendar.getInstance();
				c.add(Calendar.MINUTE, 2);
				fieldValue = c.getTime();
			}
		} else if (field.getType().equals(String.class)) {
			fieldValue = setAlternateValue ? "old" : "new";
		} else if (field.getType().equals(Person.class)) {
			//setPerson updates the personId, so we want the personIds to match for the tests to be valid
			fieldValue = new Person(setAlternateValue ? 10 : 17);
		} else if (field.getType().equals(ComplexData.class)) {
			fieldValue = new ComplexData(setAlternateValue ? "some complex data" : "Some other value", new Object());
		} else if (field.getType().equals(Obs.Interpretation.class)) {
			fieldValue = setAlternateValue ? Obs.Interpretation.ABNORMAL : Obs.Interpretation.CRITICALLY_ABNORMAL;
		} else if (field.getType().equals(Obs.Status.class)) {
			fieldValue = setAlternateValue ? Obs.Status.AMENDED : Obs.Status.PRELIMINARY;
		} else {
			fieldValue = field.getType().newInstance();
		}
		assertNotNull("Failed to generate a value for field: Obs." + field.getName());
		
		return fieldValue;
	}
	
	/**
	 * Tests the addToGroup method in ObsGroup
	 * 
	 * @throws Exception
	 */
	@Test
	public void shouldAddandRemoveObsToGroup() throws Exception {
		
		Obs obs = new Obs(1);
		
		Obs obsGroup = new Obs(755);
		
		// These methods should not fail even with null attributes on the obs
		assertFalse(obsGroup.isObsGrouping());
		assertFalse(obsGroup.hasGroupMembers(false));
		assertFalse(obsGroup.hasGroupMembers(true)); // Check both flags for false
		
		// adding an obs when the obs group has no other obs
		// should not throw an error
		obsGroup.addGroupMember(obs);
		assertEquals(1, obsGroup.getGroupMembers().size());
		
		// check duplicate add. should only be one
		obsGroup.addGroupMember(obs);
		assertTrue(obsGroup.hasGroupMembers(false));
		assertEquals(1, obsGroup.getGroupMembers().size(), "Duplicate add should not increase the grouped obs size");
		
		Obs obs2 = new Obs(2);
		
		obsGroup.removeGroupMember(obs2);
		assertTrue(obsGroup.hasGroupMembers(false));
		assertEquals(1, obsGroup.getGroupMembers().size(), "Removing a non existent obs should not decrease the number of grouped obs");
		
		// testing removing an obs from a group that has a null obs list
		new Obs().removeGroupMember(obs2);
		
		obsGroup.removeGroupMember(obs);
		
		assertEquals(0, obsGroup.getGroupMembers().size());
		
		// try to add an obs group to itself
		assertThrows(APIException.class, () -> obsGroup.addGroupMember(obsGroup));
	}
	
	/**
	 * tests the getRelatedObservations method:
	 */
	@Test
	public void shouldGetRelatedObservations() throws Exception {
		// create a child Obs
		Obs o = new Obs();
		o.setDateCreated(new Date());
		o.setLocation(new Location(1));
		o.setObsDatetime(new Date());
		o.setPerson(new Patient(2));
		o.setValueText("childObs");
		
		// create its sibling
		Obs oSibling = new Obs();
		oSibling.setDateCreated(new Date());
		oSibling.setLocation(new Location(1));
		oSibling.setObsDatetime(new Date());
		oSibling.setValueText("childObs2");
		oSibling.setPerson(new Patient(2));
		
		// create a parent Obs
		Obs oParent = new Obs();
		oParent.setDateCreated(new Date());
		oParent.setLocation(new Location(1));
		oParent.setObsDatetime(new Date());
		oSibling.setValueText("parentObs");
		oParent.setPerson(new Patient(2));
		
		// create a grandparent obs
		Obs oGrandparent = new Obs();
		oGrandparent.setDateCreated(new Date());
		oGrandparent.setLocation(new Location(1));
		oGrandparent.setObsDatetime(new Date());
		oGrandparent.setPerson(new Patient(2));
		oSibling.setValueText("grandParentObs");
		
		oParent.addGroupMember(o);
		oParent.addGroupMember(oSibling);
		oGrandparent.addGroupMember(oParent);
		
		// create a leaf observation at the grandparent level
		Obs o2 = new Obs();
		o2.setDateCreated(new Date());
		o2.setLocation(new Location(1));
		o2.setObsDatetime(new Date());
		o2.setPerson(new Patient(2));
		o2.setValueText("grandparentLeafObs");
		
		oGrandparent.addGroupMember(o2);
		
		/**
		 * test to make sure that if the original child obs calls getRelatedObservations, it returns
		 * itself and its siblings: original obs is one of two groupMembers, so relatedObservations
		 * should return a size of set 2 then, make sure that if oParent calls
		 * getRelatedObservations, it returns its own children as well as the leaf obs attached to
		 * the grandparentObs oParent has two members, and one leaf ancestor -- so a set of size 3
		 * should be returned.
		 */
		assertEquals(o.getRelatedObservations().size(), 2);
		assertEquals(oParent.getRelatedObservations().size(), 3);
		
		// create a great-grandparent obs
		Obs oGGP = new Obs();
		oGGP.setDateCreated(new Date());
		oGGP.setLocation(new Location(1));
		oGGP.setObsDatetime(new Date());
		oGGP.setPerson(new Patient(2));
		oGGP.setValueText("grandParentObs");
		oGGP.addGroupMember(oGrandparent);
		
		// create a leaf great-grandparent obs
		Obs oGGPleaf = new Obs();
		oGGPleaf.setDateCreated(new Date());
		oGGPleaf.setLocation(new Location(1));
		oGGPleaf.setObsDatetime(new Date());
		oGGPleaf.setPerson(new Patient(2));
		oGGPleaf.setValueText("grandParentObs");
		oGGP.addGroupMember(oGGPleaf);
		
		/**
		 * now run the previous assertions again. this time there are two ancestor leaf obs, so the
		 * first assertion should still return a set of size 2, but the second assertion sould
		 * return a set of size 4.
		 */
		assertEquals(o.getRelatedObservations().size(), 2);
		assertEquals(oParent.getRelatedObservations().size(), 4);
		
		// remove the grandparent leaf observation:
		
		oGrandparent.removeGroupMember(o2);
		
		// now the there is only one ancestor leaf obs:
		assertEquals(o.getRelatedObservations().size(), 2);
		assertEquals(oParent.getRelatedObservations().size(), 3);
		
		/**
		 * finally, test a non-obsGroup and non-member Obs to the function Obs o2 is now not
		 * connected to our heirarchy: an empty set should be returned:
		 */
		
		assertNotNull(o2.getRelatedObservations());
		assertEquals(o2.getRelatedObservations().size(), 0);
		
	}
	
	/**
	 * @see Obs#isComplex()
	 */
	@Test
	public void isComplex_shouldReturnTrueIfTheConceptIsComplex() throws Exception {
		ConceptDatatype cd = new ConceptDatatype();
		cd.setName("Complex");
		cd.setHl7Abbreviation("ED");
		
		ConceptComplex complexConcept = new ConceptComplex();
		complexConcept.setDatatype(cd);
		
		Obs obs = new Obs();
		obs.setConcept(complexConcept);
		
		assertTrue(obs.isComplex());
	}
	
	/**
	 * @see Obs#setValueAsString(String)
	 */
	@Test
	public void setValueAsString_shouldFailIfTheValueOfTheStringIsEmpty() throws Exception {
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("ST");
		Concept c = new Concept();
		c.setDatatype(cdt);

		Obs obs = new Obs();
		obs.setConcept(c);
		assertThrows(RuntimeException.class, () -> obs.setValueAsString(""));
	}
	
	/**
	 * @see Obs#setValueAsString(String)
	 */
	@Test
	public void setValueAsString_shouldFailIfTheValueOfTheStringIsNull() throws Exception {
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("ST");
		Concept c = new Concept();
		c.setDatatype(cdt);

		Obs obs = new Obs();
		obs.setConcept(c);
		assertThrows(RuntimeException.class, () -> obs.setValueAsString(null));
	}

	/**
	 * @see Obs#setValueAsString(String)
	 */
	@Test
	public void setValueAsString_shouldNotFailIfTheValueOfTheStringIsBlank() throws Exception {
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("ST");
		Concept c = new Concept();
		c.setDatatype(cdt);
		
		Obs obs = new Obs();
		obs.setConcept(c);
		obs.setValueAsString("\r\n");
		
		assertEquals("\r\n", obs.getValueText());
	}
	
	/**
	 * @see Obs#getValueAsBoolean()
	 */
	@Test
	public void getValueAsBoolean_shouldReturnFalseForValue_numericConceptsIfValueIs0() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(0.0);
		assertFalse(obs.getValueAsBoolean());
	}
	
	/**
	 * @see Obs#getValueAsBoolean()
	 */
	@Test
	public void getValueAsBoolean_shouldReturnNullForValue_numericConceptsIfValueIsNeither1Nor0() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(24.8);
		assertNull(obs.getValueAsBoolean());
	}
	
	@Test
	public void getValueAsString_shouldReturnNonPreciseValuesForNumericConcepts() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(25.125);
		ConceptNumeric cn = new ConceptNumeric();
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("NM");
		cn.setDatatype(cdt);
		cn.setAllowDecimal(false);
		obs.setConcept(cn);
		String str = "25";
		assertEquals(str, obs.getValueAsString(Locale.US));
	}
	
	@Test
	public void getValueAsString_shouldNotReturnLongDecimalNumbersAsScientificNotation() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(123456789.0);
		String str = "123456789.0";
		assertEquals(str, obs.getValueAsString(Locale.US));
	}
	
	@Test
	public void getValueAsString_shouldReturnDateInCorrectFormat() throws Exception {
		Obs obs = new Obs();
		obs.setValueDatetime(new Date());
		Concept cn = new Concept();
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("DT");
		cn.setDatatype(cdt);
		obs.setConcept(cn);
		
		Date utilDate = new Date();
		DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
		String dateString = dateFormat.format(utilDate);
		assertEquals(dateString, obs.getValueAsString(Locale.US));
	}
	
	/**
	 * @see Obs#getValueAsBoolean()
	 */
	@Test
	public void getValueAsBoolean_shouldReturnTrueForValue_numericConceptsIfValueIs1() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(1.0);
		assertTrue(obs.getValueAsBoolean());
	}
	
	/**
	 * @see Obs#getGroupMembers(boolean)
	 */
	@Test
	public void getGroupMembers_shouldGetAllGroupMembersIfPassedTrueAndNonvoidedIfPassedFalse() throws Exception {
		Obs parent = new Obs(1);
		Set<Obs> members = new HashSet<>();
		members.add(new Obs(101));
		members.add(new Obs(103));
		Obs voided = new Obs(99);
		voided.setVoided(true);
		members.add(voided);
		parent.setGroupMembers(members);
		members = parent.getGroupMembers(true);
		assertEquals(3, members.size(), "set of all members should have length of 3");
		members = parent.getGroupMembers(false);
		assertEquals(2, members.size(), "set of non-voided should have length of 2");
		members = parent.getGroupMembers(); // should be same as false
		assertEquals(2, members.size(), "default should return non-voided with length of 2");
	}
	
	/**
	 * @see Obs#hasGroupMembers(boolean)
	 */
	@Test
	public void hasGroupMembers_shouldReturnTrueIfThisObsHasGroupMembersBasedOnParameter() throws Exception {
		Obs parent = new Obs(5);
		Obs child = new Obs(33);
		child.setVoided(true);
		parent.addGroupMember(child); // Only contains 1 voided child
		assertTrue(parent.hasGroupMembers(true), "When checking for all members, should return true");
		assertFalse(parent.hasGroupMembers(false), "When checking for non-voided, should return false");
		assertFalse(parent.hasGroupMembers(), "Default should check for non-voided");
	}
	
	/**
	 * @see Obs#isObsGrouping()
	 */
	@Test
	public void isObsGrouping_shouldIncludeVoidedObs() throws Exception {
		Obs parent = new Obs(5);
		Obs child = new Obs(33);
		child.setVoided(true);
		parent.addGroupMember(child);
		assertTrue(parent.isObsGrouping(), "When checking for Obs grouping, should include voided Obs");
	}
	
	/**
	 * @see Obs#getValueAsString(Locale)
	 */
	@Test
	public void getValueAsString_shouldUseCommasOrDecimalPlacesDependingOnLocale() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(123456789.3);
		String str = "123456789,3";
		assertEquals(str, obs.getValueAsString(Locale.GERMAN));
	}
	
	/**
	 * @see Obs#getValueAsString(Locale)
	 */
	@Test
	public void getValueAsString_shouldNotUseThousandSeparator() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(123456789.0);
		String str = "123456789.0";
		assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
	}
	
	/**
	 * @see Obs#getValueAsString(Locale)
	 */
	@Test
	public void getValueAsString_shouldReturnRegularNumberForSizeOfZeroToOrGreaterThanTenDigits() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(1234567890.0);
		String str = "1234567890.0";
		assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
	}
	
	/**
	 * @see Obs#getValueAsString(Locale)
	 */
	@Test
	public void getValueAsString_shouldReturnRegularNumberIfDecimalPlacesAreAsHighAsSix() throws Exception {
		Obs obs = new Obs();
		obs.setValueNumeric(123456789.012345);
		String str = "123456789.012345";
		assertEquals(str, obs.getValueAsString(Locale.ENGLISH));
	}
	
	@Test
	public void getValueAsString_shouldReturnLocalizedCodedConcept() throws Exception {
		ConceptDatatype cdt = new ConceptDatatype();
		cdt.setHl7Abbreviation("CWE");
		
		Concept cn = new Concept();
		cn.setDatatype(cdt);
		cn.addName(new ConceptName(VERO, Locale.ITALIAN));
		
		Obs obs = new Obs();
		obs.setValueCoded(cn);
		obs.setConcept(cn);
		obs.setValueCodedName(new ConceptName("True", Locale.US));
		
		assertEquals(VERO, obs.getValueAsString(Locale.ITALIAN));
	}
	
	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnFalseWhenNoChangeHasBeenMade() throws Exception {
		assertFalse(new Obs().isDirty());
		
		//Should also work if setters are called with same values as the original
		Obs obs = createObs(2);
		obs.setGroupMembers(new LinkedHashSet<>());
		obs.getConcept().setDatatype(new ConceptDatatype());
		assertFalse(obs.isDirty());
		BeanUtils.copyProperties(obs, BeanUtils.cloneBean(obs));
		assertFalse(obs.isDirty());

		obs = createObs(null);
		obs.setGroupMembers(new LinkedHashSet<>());
		obs.getConcept().setDatatype(new ConceptDatatype());
		assertFalse(obs.isDirty());
		BeanUtils.copyProperties(obs, BeanUtils.cloneBean(obs));
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnTrueWhenAnyImmutableFieldHasBeenChangedForEditedObs() throws Exception {
		Obs obs = createObs(2);
		assertFalse(obs.isDirty());
		updateImmutableFieldsAndAssert(obs, true);
	}

	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnFalseWhenAnyImmutableFieldHasBeenChangedForNewObs() throws Exception {
		Obs obs = createObs(null);
		assertFalse(obs.isDirty());
		updateImmutableFieldsAndAssert(obs, false);
	}

	private void updateImmutableFieldsAndAssert(Obs obs, boolean assertion) throws Exception {
		//Set all fields to some random values via reflection
		List<Field> fields = Reflect.getAllFields(Obs.class);

		final Integer originalPersonId = obs.getPersonId();
		//call each setter and check that dirty has been set to true for each
		for (Field field : fields) {
			String fieldName = field.getName();
			if (IGNORED_FIELDS.contains(fieldName)) {
				continue;
			}

			if ("personId".equals(fieldName)) {
				//call setPersonId because it is protected so BeanUtils.setProperty won't work
				obs.setPersonId((Integer) generateValue(field, true));
			} else {
				BeanUtils.setProperty(obs, fieldName, generateValue(field, true));
			}
			assertEquals(obs.isDirty(), assertion, "Obs was not marked as dirty after changing: " + fieldName);
			if ("person".equals(fieldName)) {
				//Because setPerson updates the personId we need to reset personId to its original value 
				//that matches that of person otherwise the test will fail for the personId field
				obs.setPersonId(originalPersonId);
			}
			
			//reset for next field
			resetObs(obs);
		}
	}
	
	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnFalseWhenOnlyMutableFieldsAreChanged() throws Exception {
		Obs obs = new Obs();
		obs.setVoided(true);
		obs.setVoidedBy(new User(1000));
		obs.setVoidReason("some other reason");
		obs.setDateVoided(new Date());
		assertFalse(obs.isDirty());

		Obs obsEdited = new Obs(5);
		obsEdited.setVoided(true);
		obsEdited.setVoidedBy(new User(1000));
		obsEdited.setVoidReason("some other reason");
		obsEdited.setDateVoided(new Date());
		assertFalse(obsEdited.isDirty());
	}


	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnTrueWhenAnImmutableFieldIsChangedFromANonNullToANullValueForEditedObs() throws Exception {
		Obs obs = createObs(2);
		assertNotNull(obs.getComment());
		obs.setComment(null);
		assertTrue(obs.isDirty());
	}

	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnFalsWhenAnImmutableFieldIsChangedFromANonNullToANullValueForNewObs() throws Exception {
		Obs obs = createObs(null);
		assertNotNull(obs.getComment());
		obs.setComment(null);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnTrueWhenAnImmutableFieldIsChangedFromANullToANonNullValueInExistingObs() throws Exception {
		Obs obs = new Obs(5);
		assertNull(obs.getComment());
		obs.setComment("some non null value");
		assertTrue(obs.isDirty());
	}

	/**
	 * @see Obs#isDirty()
	 */
	@Test
	public void isDirty_shouldReturnFalseWhenAnImmutableFieldIsChangedFromANullToANonNullValueInNewObs() throws Exception {
		Obs obs = new Obs();
		assertNull(obs.getComment());
		obs.setComment("some non null value");
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#setFormField(String,String)
	 */
	@Test
	public void setFormField_shouldNotMarkTheObsAsDirtyWhenTheValueHasNotBeenChanged() throws Exception {
		Obs obs = createObs(3);
		obs.setFormField(obs.getFormFieldNamespace(), obs.getFormFieldPath());
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#setFormField(String,String)
	 */
	@Test
	public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueHasBeenChanged() throws Exception {
		Obs obs = createObs(5);
		final String newNameSpace = "someNameSpace";
		final String newPath = "somePath";
		assertNotEquals(newPath, obs.getFormFieldNamespace());
		assertNotEquals(newNameSpace, obs.getFormFieldPath());
		obs.setFormField(newNameSpace, newPath);
		assertTrue(obs.isDirty());
	}
	
	/**
	 * @see Obs#setFormField(String,String)
	 */
	@Test
	public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueIsChangedFromANonNullToANullValue() throws Exception {
		Obs obs = new Obs(2);
		obs.setFormField("someNameSpace", "somePath");
		resetObs(obs);
		assertFalse(obs.isDirty());
		assertNotNull(obs.getFormFieldNamespace());
		assertNotNull(obs.getFormFieldPath());
		obs.setFormField(null, null);
		assertTrue(obs.isDirty());
	}

	/**
	 * @see Obs#setFormField(String,String)
	 */
	@Test
	public void setFormField_shouldMarkTheObsAsDirtyWhenTheValueIsChangedFromANullToANonNullValue() throws Exception {
		Obs obs = new Obs(5);
		assertNull(obs.getFormFieldNamespace());
		assertNull(obs.getFormFieldPath());
		obs.setFormField("someNameSpace", "somePath");
		assertTrue(obs.isDirty());
	}
	
	/**
	 * @see Obs#addGroupMember(Obs)
	 */
	@Test
	public void addGroupMember_shouldReturnFalseWhenADuplicateObsIsAddedAsAMember() throws Exception {
		Obs obs = new Obs(2);
		Obs member = new Obs();
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
		resetObs(obs);
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#addGroupMember(Obs)
	 */
	@Test
	public void addGroupMember_shouldReturnFalseWhenADuplicateObsIsAddedAsAMemberToNewObs() throws Exception {
		Obs obs = new Obs();
		Obs member = new Obs();
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
		resetObs(obs);
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#addGroupMember(Obs)
	 */
	@Test
	public void addGroupMember_shouldReturnFalseWhenANewObsIsAddedAsAMember() throws Exception {
		Obs obs = new Obs(2);
		Obs member1 = new Obs();
		obs.addGroupMember(member1);
		assertFalse(obs.isDirty());
		resetObs(obs);
		Obs member2 = new Obs();
		obs.addGroupMember(member2);
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#removeGroupMember(Obs)
	 */
	@Test
	public void removeGroupMember_shouldReturnFalseWhenANonExistentObsIsRemoved() throws Exception {
		Obs obs = new Obs();
		obs.removeGroupMember(new Obs());
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#removeGroupMember(Obs)
	 */
	@Test
	public void removeGroupMember_shouldReturnDirtyFalseWhenAnObsIsRemoved() throws Exception {
		Obs obs = new Obs(2);
		Obs member = new Obs();
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
		resetObs(obs);
		obs.removeGroupMember(member);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#removeGroupMember(Obs)
	 */
	@Test
	public void removeGroupMember_shouldReturnFalseForDirtyFlagWhenAnObsIsRemovedFromGroup() throws Exception {
		Obs obs = new Obs();
		Obs member = new Obs();
		obs.addGroupMember(member);
		assertFalse(obs.isDirty());
		resetObs(obs);
		obs.removeGroupMember(member);
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkTheExistingObsAsDirtyWhenTheSetIsChangedFromNullToANonEmptyOne() throws Exception {
		Obs obs = new Obs(5);
		assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
		Set<Obs> members = new HashSet<>();
		members.add(new Obs());
		obs.setGroupMembers(members);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkNewObsAsDirtyWhenTheSetIsChangedFromNullToANonEmptyOne() throws Exception {
		Obs obs = new Obs();
		assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
		Set<Obs> members = new HashSet<>();
		members.add(new Obs());
		obs.setGroupMembers(members);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkTheExistingObsAsDirtyWhenTheSetIsReplacedWithAnotherWithDifferentMembers()
	    throws Exception {
		Obs obs = new Obs(2);
		Set<Obs> members1 = new HashSet<>();
		members1.add(new Obs());
		obs.setGroupMembers(members1);
		resetObs(obs);
		Set<Obs> members2 = new HashSet<>();
		members2.add(new Obs());
		obs.setGroupMembers(members2);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkTheNewObsAsDirtyWhenTheSetIsReplacedWithAnotherWithDifferentMembers()
			throws Exception {
		Obs obs = new Obs();
		Set<Obs> members1 = new HashSet<>();
		members1.add(new Obs());
		obs.setGroupMembers(members1);
		assertFalse(obs.isDirty());
		Set<Obs> members2 = new HashSet<>();
		members2.add(new Obs());
		obs.setGroupMembers(members2);
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkTheObsAsDirtyWhenTheSetIsChangedFromNullToAnEmptyOne() throws Exception {
		Obs obs = new Obs();
		assertNull(Obs.class.getDeclaredField("groupMembers").get(obs));
		obs.setGroupMembers(new HashSet<>());
		assertFalse(obs.isDirty());
	}
	
	/**
	 * @see Obs#setGroupMembers(Set)
	 */
	@Test
	public void setGroupMembers_shouldNotMarkTheObsAsDirtyWhenTheSetIsReplacedWithAnotherWithSameMembers() throws Exception {
		Obs obs = new Obs();
		Obs o = new Obs();
		Set<Obs> members1 = new HashSet<>();
		members1.add(o);
		obs.setGroupMembers(members1);
		resetObs(obs);
		Set<Obs> members2 = new HashSet<>();
		members2.add(o);
		obs.setGroupMembers(members2);
		assertFalse(obs.isDirty());
	}

	/**
	 * @see Obs#setObsDatetime(Date)
	 */
	@Test
	public void setObsDateTime_shouldNotMarkTheObsAsDirtyWhenDateIsNotChangedAndExistingValueIsOfTimeStampType(){
		Obs obs = new Obs();
		Date date = new Date();
		Timestamp timestamp = new Timestamp(date.getTime());
		obs.setObsDatetime(timestamp);
		obs.setId(1);
		assertFalse(obs.isDirty());

		obs.setObsDatetime(date);

		assertFalse(obs.isDirty());
	}
	
	@Test
	public void shouldSetFinalStatusOnNewObsByDefault() throws Exception {
		Obs obs = new Obs();
		assertThat(obs.getStatus(), is(Obs.Status.FINAL));
	}
	
	@Test
	public void newInstance_shouldCopyMostFields() throws Exception {
		Obs obs = new Obs();
		obs.setStatus(Obs.Status.PRELIMINARY);
		obs.setInterpretation(Obs.Interpretation.LOW);
		obs.setConcept(new Concept());
		obs.setValueNumeric(1.2);
		obs.setObsGroup(new  Obs());        
        obs.setAccessionNumber("4849RDD");          
        obs.setValueCoded(new Concept());                  
        obs.setValueDrug(new Drug());                      
        obs.setValueGroupId(new  Integer(23));
        obs.setValueDatetime(new Date());
        obs.setValueModifier("djfsihdihd");              
        obs.setValueText("xyzABC");
        obs.setComment("This is an example");
        obs.setEncounter(new Encounter(3));  
        obs.setCreator( new  User(1));                    
        obs.setDateCreated(new Date());        
        obs.setVoided(false);            
        obs.setVoidedBy(new  User(2));      
        obs.setDateVoided(new Date());                                
        obs.setVoidReason("Some reason");    
        obs.setValueComplex("SDIODF7980IDSISF");
        obs.setComplexData(new ComplexData("Complex data", "Complex data".getBytes()));
		
		Obs copy = Obs.newInstance(obs);
		
		// these fields are not copied
		assertThat(copy.getObsId(), nullValue());
		assertThat(copy.getUuid(), not(obs.getUuid()));
		
		// other fields are copied
		assertThat(copy.getConcept(), is(obs.getConcept()));
		assertThat(copy.getValueNumeric(), is(obs.getValueNumeric()));
		assertThat(copy.getStatus(), is(obs.getStatus()));
		assertThat(copy.getInterpretation(), is(obs.getInterpretation()));
		assertThat(copy.getConcept(), is(obs.getConcept()));
		assertThat(copy.getAccessionNumber(),  is(obs.getAccessionNumber()));                          
		assertThat(copy.getValueDrug(),  is(obs.getValueDrug()));                      
		assertThat(copy.getValueGroupId(),  is(obs.getValueGroupId()));
		assertThat(copy.getValueDatetime(),  is(obs.getValueDatetime()));
		assertThat(copy.getValueNumeric(),  is(obs.getValueNumeric()));      
		assertThat(copy.getValueModifier(),  is(obs.getValueModifier()));              
		assertThat(copy.getValueText(),  is(obs.getValueText()));
		assertThat(copy.getComment(),  is(obs.getComment()));
		assertThat(copy.getEncounter(),  is(obs.getEncounter()));
		assertThat(copy.getCreator(),  is(obs.getCreator()));                  
		assertThat(copy.getDateCreated(),  is(obs.getDateCreated()));        
		assertThat(copy.getVoided(),  is(obs.getVoided()));          
		assertThat(copy.getVoidedBy(),  is(obs.getVoidedBy()));    
		assertThat(copy.getDateVoided(),  is(obs.getDateVoided()));                              
		assertThat(copy.getVoidReason(),  is(obs.getVoidReason()));    
		assertThat(copy.getValueComplex(),  is(obs.getValueComplex()));
		assertThat(copy.getComplexData(),  is(obs.getComplexData()));

	}
	
	@Test
	public void shouldSupportInterpretationProperty() throws Exception {
		Obs obs = new Obs();
		assertThat(obs.getInterpretation(), nullValue());

		obs.setInterpretation(Obs.Interpretation.NORMAL);
		assertThat(obs.getInterpretation(), is(Obs.Interpretation.NORMAL));
	}
	
	@Test
	public void setValueBoolean_shouldNotSetValueForNonBooleanConcept() throws Exception {
		Obs obs = createObs(2);
		ConceptDatatype dataType = new ConceptDatatype();
		dataType.setUuid(ConceptDatatype.CODED_UUID);
		obs.getConcept().setDatatype(dataType);
		assertNotNull(obs.getValueCoded());
		obs.setValueBoolean(null);
		assertNotNull(obs.getValueCoded());
	}
}
