
package com.evolveum.midpoint.prism;

import static com.evolveum.midpoint.prism.PrismInternalTestUtil.*;

import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.assertEquals;
import java.io.IOException;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.transform.dom.DOMSource;
import javax.xml.validation.Schema;
import javax.xml.validation.Validator;

import org.testng.annotations.BeforeSuite;
import org.testng.annotations.Test;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import com.evolveum.midpoint.prism.foo.ActivationType;
import com.evolveum.midpoint.prism.foo.AssignmentType;
import com.evolveum.midpoint.prism.foo.UserType;
import com.evolveum.midpoint.prism.util.PrismAsserts;
import com.evolveum.midpoint.util.DOMUtil;
import com.evolveum.midpoint.util.PrettyPrinter;
import com.evolveum.midpoint.util.exception.SchemaException;

/**
 * @author semancik
 *
 */
public class TestPrismObjectConstruction {
	
	private static final String USER_OID = "1234567890";
	private static final String ACCOUNT1_OID = "11100000111";
	private static final String ACCOUNT2_OID = "11100000222";
	
	
	@BeforeSuite
	public void setupDebug() {
		PrettyPrinter.setDefaultNamespacePrefix(DEFAULT_NAMESPACE_PREFIX);
	}

    /**
     * Construct object with schema. Starts by instantiating a definition and working downwards.
     * All the items in the object should have proper definition.
     */
	@Test
	public void testConstructionWithSchema() throws Exception {
		final String TEST_NAME = "testConstructionWithSchema";
		PrismInternalTestUtil.displayTestTitle(TEST_NAME);
		
		// GIVEN
		PrismContext ctx = constructInitializedPrismContext();
		PrismObjectDefinition<UserType> userDefinition = getFooSchema(ctx).findObjectDefinitionByElementName(new QName(NS_FOO,"user"));
		
		// WHEN
		PrismObject<UserType> user = userDefinition.instantiate();
		// Fill-in object values, checking presence of definition while doing so
		fillInUserDrake(user, true);
		
		// THEN
		System.out.println("User:");
		System.out.println(user.debugDump());
		// Check if the values are correct, also checking definitions
		assertUserDrake(user, true, ctx);
	}
	
	/**
	 * Construct object without schema. Starts by creating object "out of the blue" and
	 * the working downwards. 
	 */
	@Test(enabled = false)			// definition-less containers are no longer supported
	public void testDefinitionlessConstruction() throws Exception {
		final String TEST_NAME = "testDefinitionlessConstruction";
		PrismInternalTestUtil.displayTestTitle(TEST_NAME);

		// GIVEN
		// No context needed
		
		// WHEN
		PrismObject<UserType> user = new PrismObject<UserType>(USER_QNAME, UserType.class);
		// Fill-in object values, no schema checking
		fillInUserDrake(user, false);
		
		// THEN
		System.out.println("User:");
		System.out.println(user.debugDump());
		// Check if the values are correct, no schema checking
		PrismContext ctx = constructInitializedPrismContext();
		assertUserDrake(user, false, ctx);
	}
	
	/**
	 * Construct object without schema. Starts by creating object "out of the blue" and
	 * the working downwards. Then apply the schema. Check definitions.
	 */
//	@Test
	public void testDefinitionlessConstructionAndSchemaApplication() throws Exception {
		final String TEST_NAME = "testDefinitionlessConstructionAndSchemaApplication";
		PrismInternalTestUtil.displayTestTitle(TEST_NAME);

		// GIVEN
		// No context needed (yet)
		PrismObject<UserType> user = new PrismObject<UserType>(USER_QNAME, UserType.class);
		// Fill-in object values, no schema checking
		fillInUserDrake(user, false);
		// Make sure the object is OK
		PrismContext ctx = constructInitializedPrismContext();
		assertUserDrake(user, false, ctx);
		
		
		PrismObjectDefinition<UserType> userDefinition =
				getFooSchema(ctx).findObjectDefinitionByElementName(new QName(NS_FOO,"user"));
		
		// WHEN
		user.applyDefinition(userDefinition);
			
		// THEN
		System.out.println("User:");
		System.out.println(user.debugDump());
		
		// Check schema now 
		assertUserDrake(user, true, ctx);

	}
	
	@Test
	public void testClone() throws Exception {
		final String TEST_NAME = "testClone";
		PrismInternalTestUtil.displayTestTitle(TEST_NAME);
		
		// GIVEN
		PrismContext ctx = constructInitializedPrismContext();
		PrismObjectDefinition<UserType> userDefinition = getFooSchema(ctx).findObjectDefinitionByElementName(new QName(NS_FOO,"user"));
		PrismObject<UserType> user = userDefinition.instantiate();
		fillInUserDrake(user, true);
		// precondition
		assertUserDrake(user, true, ctx);

		// WHEN
		PrismObject<UserType> clone = user.clone();
		
		// THEN
		System.out.println("Cloned user:");
		System.out.println(clone.debugDump());
		// Check if the values are correct, also checking definitions
		assertUserDrake(clone, true, ctx);
	}

	@Test
	public void testCloneEquals() throws Exception {
		final String TEST_NAME = "testCloneEquals";
		PrismInternalTestUtil.displayTestTitle(TEST_NAME);
		
		// GIVEN
		PrismContext ctx = constructInitializedPrismContext();
		PrismObjectDefinition<UserType> userDefinition = getFooSchema(ctx).findObjectDefinitionByElementName(new QName(NS_FOO,"user"));
		PrismObject<UserType> user = userDefinition.instantiate();
		fillInUserDrake(user, true);
		PrismObject<UserType> clone = user.clone();
		
		// WHEN, THEN
		assertTrue("Clone not equal", clone.equals(user));
		assertTrue("Clone not equivalent", clone.equivalent(user));
	}

	
	private void fillInUserDrake(PrismObject<UserType> user, boolean assertDefinitions) throws SchemaException {
		user.setOid(USER_OID);
		
		// fullName
		PrismProperty<String> fullNameProperty = user.findOrCreateProperty(USER_FULLNAME_QNAME);
		assertEquals(USER_FULLNAME_QNAME, fullNameProperty.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(fullNameProperty, DOMUtil.XSD_STRING, 1, 1);
		fullNameProperty.setValue(new PrismPropertyValue<String>("Sir Fancis Drake"));
		PrismProperty<String> fullNamePropertyAgain = user.findOrCreateProperty(USER_FULLNAME_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Property not the same", fullNameProperty == fullNamePropertyAgain);
		
		// activation
		PrismContainer<ActivationType> activationContainer = user.findOrCreateContainer(USER_ACTIVATION_QNAME);
		assertEquals(USER_ACTIVATION_QNAME, activationContainer.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(activationContainer, ACTIVATION_TYPE_QNAME, 0, 1);
		PrismContainer<ActivationType> activationContainerAgain = user.findOrCreateContainer(USER_ACTIVATION_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Property not the same", activationContainer == activationContainerAgain);
		
		// activation/enabled
		PrismProperty<Boolean> enabledProperty = user.findOrCreateProperty(USER_ENABLED_PATH);
		assertEquals(USER_ENABLED_QNAME, enabledProperty.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(enabledProperty, DOMUtil.XSD_BOOLEAN, 0, 1);
		enabledProperty.setValue(new PrismPropertyValue<Boolean>(true));
		PrismProperty<Boolean> enabledPropertyAgain = activationContainer.findOrCreateProperty(USER_ENABLED_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Property not the same", enabledProperty == enabledPropertyAgain);
		
		// assignment
		// Try to create this one from the value. It should work the same, but let's test a different code path
		PrismContainer<AssignmentType> assignmentContainer = user.getValue().findOrCreateContainer(USER_ASSIGNMENT_QNAME);
		assertEquals(USER_ASSIGNMENT_QNAME, assignmentContainer.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(assignmentContainer, ASSIGNMENT_TYPE_QNAME, 0, -1);
		PrismContainer<AssignmentType> assignmentContainerAgain = user.findOrCreateContainer(USER_ASSIGNMENT_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Property not the same", assignmentContainer == assignmentContainerAgain);
		assertEquals("Wrong number of assignment values (empty)", 0, assignmentContainer.getValues().size());
		
		// assignment values: construct assignment value as a new container "out of the blue" and then add it.
		PrismContainer<AssignmentType> assBlueContainer = new PrismContainer<AssignmentType>(USER_ASSIGNMENT_QNAME);
		PrismProperty<String> assBlueDescriptionProperty = assBlueContainer.findOrCreateProperty(USER_DESCRIPTION_QNAME);
		assBlueDescriptionProperty.addValue(new PrismPropertyValue<String>("Assignment created out of the blue"));
		PrismAsserts.assertParentConsistency(user);
		assignmentContainer.mergeValues(assBlueContainer);
		assertEquals("Wrong number of assignment values (after blue)", 1, assignmentContainer.getValues().size());
		PrismAsserts.assertParentConsistency(user);
		
		// assignment values: construct assignment value as a new container value "out of the blue" and then add it.
		PrismContainerValue<AssignmentType> assCyanContainerValue = new PrismContainerValue<AssignmentType>();
		PrismProperty<String> assCyanDescriptionProperty = assCyanContainerValue.findOrCreateProperty(USER_DESCRIPTION_QNAME);
		assCyanDescriptionProperty.addValue(new PrismPropertyValue<String>("Assignment created out of the cyan"));
		assignmentContainer.mergeValue(assCyanContainerValue);
		assertEquals("Wrong number of assignment values (after cyan)", 2, assignmentContainer.getValues().size());
		PrismAsserts.assertParentConsistency(user);
		
		// assignment values: construct assignment value from existing container
		PrismContainerValue<AssignmentType> assRedContainerValue = assignmentContainer.createNewValue();
		PrismProperty<String> assRedDescriptionProperty = assRedContainerValue.findOrCreateProperty(USER_DESCRIPTION_QNAME);
		assRedDescriptionProperty.addValue(new PrismPropertyValue<String>("Assignment created out of the red"));
		assertEquals("Wrong number of assignment values (after red)", 3, assignmentContainer.getValues().size());
		PrismAsserts.assertParentConsistency(user);
		
		// accountRef
		PrismReference accountRef = user.findOrCreateReference(USER_ACCOUNTREF_QNAME);
		assertEquals(USER_ACCOUNTREF_QNAME, accountRef.getElementName());
		if (assertDefinitions) PrismAsserts.assertDefinition(accountRef, OBJECT_REFERENCE_TYPE_QNAME, 0, -1);
		accountRef.add(new PrismReferenceValue(ACCOUNT1_OID));
		accountRef.add(new PrismReferenceValue(ACCOUNT2_OID));
		PrismReference accountRefAgain = user.findOrCreateReference(USER_ACCOUNTREF_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Property not the same", accountRef == accountRefAgain);
		assertEquals("accountRef size", 2, accountRef.getValues().size());
		PrismAsserts.assertParentConsistency(user);
		
		// extension
		PrismContainer<?> extensionContainer = user.findOrCreateContainer(USER_EXTENSION_QNAME);
		assertEquals(USER_EXTENSION_QNAME, extensionContainer.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(extensionContainer, DOMUtil.XSD_ANY, 0, 1);
		PrismContainer<AssignmentType> extensionContainerAgain = user.findOrCreateContainer(USER_EXTENSION_QNAME);
		// The "==" is there by purpose. We really want to make sure that is the same *instance*, that is was not created again
		assertTrue("Extension not the same", extensionContainer == extensionContainerAgain);
		assertEquals("Wrong number of extension values (empty)", 0, extensionContainer.getValues().size());
		
		// extension / stringType
		PrismProperty<String> stringTypeProperty = extensionContainer.findOrCreateProperty(EXTENSION_STRING_TYPE_ELEMENT);
		assertEquals(EXTENSION_STRING_TYPE_ELEMENT, stringTypeProperty.getElementName());
		PrismAsserts.assertParentConsistency(user);
		if (assertDefinitions) PrismAsserts.assertDefinition(stringTypeProperty, DOMUtil.XSD_STRING, 0, -1);
		
		// TODO
		
	}

	private void assertUserDrake(PrismObject<UserType> user, boolean assertDefinitions, PrismContext prismContext) throws SchemaException, SAXException, IOException {
		assertEquals("Wrong OID", USER_OID, user.getOid());
		assertEquals("Wrong compileTimeClass", UserType.class, user.getCompileTimeClass());
		
		user.checkConsistence();
		assertUserDrakeContent(user, assertDefinitions);
		if (assertDefinitions) {
			serializeAndValidate(user, prismContext);
		}
	}
		
	private void assertUserDrakeContent(PrismObject<UserType> user, boolean assertDefinitions) {
		// fullName
		PrismProperty fullNameProperty = user.findProperty(USER_FULLNAME_QNAME);
		if (assertDefinitions) PrismAsserts.assertDefinition(fullNameProperty, DOMUtil.XSD_STRING, 1, 1);
		assertEquals("Wrong fullname", "Sir Fancis Drake", fullNameProperty.getValue().getValue());
		// activation
		PrismContainer activationContainer = user.findContainer(USER_ACTIVATION_QNAME);
		assertEquals(USER_ACTIVATION_QNAME, activationContainer.getElementName());
		if (assertDefinitions) PrismAsserts.assertDefinition(activationContainer, ACTIVATION_TYPE_QNAME, 0, 1);
		// activation/enabled
		PrismProperty enabledProperty = user.findProperty(USER_ENABLED_PATH);
		assertEquals(USER_ENABLED_QNAME, enabledProperty.getElementName());
		if (assertDefinitions) PrismAsserts.assertDefinition(enabledProperty, DOMUtil.XSD_BOOLEAN, 0, 1);
		assertEquals("Wrong enabled", true, enabledProperty.getValue().getValue());
		// assignment
		PrismContainer assignmentContainer = user.findContainer(USER_ASSIGNMENT_QNAME);
		assertEquals(USER_ASSIGNMENT_QNAME, assignmentContainer.getElementName());
		if (assertDefinitions) PrismAsserts.assertDefinition(assignmentContainer, ASSIGNMENT_TYPE_QNAME, 0, -1);
		// assignment values
		List<PrismContainerValue> assValues = assignmentContainer.getValues();
		assertEquals("Wrong number of assignment values", 3, assValues.size());
		// assignment values: blue
		PrismContainerValue assBlueValue = assValues.get(0);
		PrismProperty assBlueDescriptionProperty = assBlueValue.findProperty(USER_DESCRIPTION_QNAME);
		if (assertDefinitions) PrismAsserts.assertDefinition(assBlueDescriptionProperty, DOMUtil.XSD_STRING, 0, 1);
		assertEquals("Wrong blue assignment description", "Assignment created out of the blue", assBlueDescriptionProperty.getValue().getValue());
		// assignment values: cyan
		PrismContainerValue assCyanValue = assValues.get(1);
		PrismProperty assCyanDescriptionProperty = assCyanValue.findProperty(USER_DESCRIPTION_QNAME);
		if (assertDefinitions) PrismAsserts.assertDefinition(assCyanDescriptionProperty, DOMUtil.XSD_STRING, 0, 1);
		assertEquals("Wrong cyan assignment description", "Assignment created out of the cyan", assCyanDescriptionProperty.getValue().getValue());
		// assignment values: red
		PrismContainerValue assRedValue = assValues.get(2);
		PrismProperty assRedDescriptionProperty = assRedValue.findProperty(USER_DESCRIPTION_QNAME);
		if (assertDefinitions) PrismAsserts.assertDefinition(assRedDescriptionProperty, DOMUtil.XSD_STRING, 0, 1);
		assertEquals("Wrong red assignment description", "Assignment created out of the red", assRedDescriptionProperty.getValue().getValue());
		// accountRef
		PrismReference accountRef = user.findReference(USER_ACCOUNTREF_QNAME);
		if (assertDefinitions) PrismAsserts.assertDefinition(accountRef, OBJECT_REFERENCE_TYPE_QNAME, 0, -1);
		PrismAsserts.assertReferenceValue(accountRef, ACCOUNT1_OID);
		PrismAsserts.assertReferenceValue(accountRef, ACCOUNT2_OID);
		assertEquals("accountRef size", 2, accountRef.getValues().size());
		PrismAsserts.assertParentConsistency(user);
	}
		
	private void serializeAndValidate(PrismObject<UserType> user, PrismContext prismContext) throws SchemaException, SAXException, IOException {
		String xmlString = prismContext.serializeObjectToString(user, PrismContext.LANG_XML);
		System.out.println("Serialized XML");
		System.out.println(xmlString);
		Document xmlDocument = DOMUtil.parseDocument(xmlString);
		Schema javaxSchema = prismContext.getSchemaRegistry().getJavaxSchema();
		Validator validator = javaxSchema.newValidator();
		validator.setResourceResolver(prismContext.getEntityResolver());
		validator.validate(new DOMSource(xmlDocument));
	}

}
