/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2014, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.controller.transform.description;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.ADD;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OP_ADDR;

import java.util.Collections;
import java.util.Map;

import org.jboss.as.controller.ExpressionResolver;
import org.jboss.as.controller.ModelVersion;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.ProcessType;
import org.jboss.as.controller.ResourceDefinition;
import org.jboss.as.controller.RunningMode;
import org.jboss.as.controller.SimpleResourceDefinition;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.descriptions.NonResolvingResourceDescriptionResolver;
import org.jboss.as.controller.operations.common.Util;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.controller.transform.OperationTransformer;
import org.jboss.as.controller.transform.OperationTransformer.TransformedOperation;
import org.jboss.as.controller.transform.ResourceTransformationContext;
import org.jboss.as.controller.transform.TransformationContext;
import org.jboss.as.controller.transform.TransformationTarget;
import org.jboss.as.controller.transform.TransformationTargetImpl;
import org.jboss.as.controller.transform.TransformerRegistry;
import org.jboss.as.controller.transform.Transformers;
import org.jboss.as.controller.transform.TransformersSubRegistration;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ValueExpression;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
 */
public class ChainedOperationBuilderTestCase {

    private static PathElement PATH = PathElement.pathElement("toto", "testSubsystem");

    private static final ResourceDefinition ROOT = new SimpleResourceDefinition(null, new NonResolvingResourceDescriptionResolver());
    private static final ModelVersion V1_0_0 = ModelVersion.create(1, 0, 0);
    private static final ModelVersion V2_0_0 = ModelVersion.create(2, 0, 0);
    private static final ModelVersion V3_0_0 = ModelVersion.create(3, 0, 0);
    private static final ModelVersion V4_0_0 = ModelVersion.create(4, 0, 0);
    private static final ModelVersion UNKNOWN_VERSION = ModelVersion.create(1, 1, 0);
    private static final ModelVersion[] ALL_TESTED_VERSIONS = {V1_0_0, ModelVersion.create(1, 0, 1), ModelVersion.create(1, 0, 5), UNKNOWN_VERSION};
    private static final ModelVersion[] VALID_TESTED_VERSIONS = {V1_0_0, ModelVersion.create(1, 0, 1), ModelVersion.create(1, 0, 5)};

    private Resource resourceRoot = Resource.Factory.create();
    private Resource toto;
    private TransformerRegistry registry = TransformerRegistry.Factory.create();
    private ManagementResourceRegistration resourceRegistration = ManagementResourceRegistration.Factory.forProcessType(ProcessType.EMBEDDED_SERVER).createRegistration(ROOT);
    private TransformersSubRegistration transformersSubRegistration;
    private ModelNode resourceModel;

    @Before
    public void setUp() {
        // Cleanup
        resourceRoot = Resource.Factory.create();
        registry = TransformerRegistry.Factory.create();
        resourceRegistration = ManagementResourceRegistration.Factory.forProcessType(ProcessType.EMBEDDED_SERVER).createRegistration(ROOT);
        // test
        toto = Resource.Factory.create();
        resourceRoot.registerChild(PATH, toto);
        resourceModel = toto.getModel();

        // Register the description
        transformersSubRegistration = registry.getServerRegistration(ModelVersion.create(1));
    }

    @Test
    public void testOperationNoTransformation() throws Exception {

        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V2_0_0);
        chainedBuilder.createBuilder(V2_0_0, V1_0_0);

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode node = new ModelNode();
        node.get(OP_ADDR).set(address.toModelNode());
        node.get(OP).set(ADD);

        OperationTransformer.TransformedOperation op;
        for(ModelVersion version : ALL_TESTED_VERSIONS) {
            op = transformOperation(node, version);
            Assert.assertFalse(op.rejectOperation(success()));
            Assert.assertEquals(node, op.getTransformedOperation());
        }

        node.get(OP_ADDR).set(address.append("one", "two").toModelNode());
        for(ModelVersion version : ALL_TESTED_VERSIONS) {
            op = transformOperation(node, version);
            Assert.assertFalse(op.rejectOperation(success()));
            Assert.assertEquals(node, op.getTransformedOperation());
        }
    }

    @Test
    public void testOperationChainedNoTransformation() throws Exception {

        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V3_0_0);
        chainedBuilder.createBuilder(V3_0_0, V2_0_0);
        chainedBuilder.createBuilder(V2_0_0, V1_0_0);

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode node = new ModelNode();
        node.get(OP_ADDR).set(address.toModelNode());
        node.get(OP).set(ADD);
        assertNotRejectedOperation(node, node);

        node.get(OP_ADDR).set(address.append("one", "two").toModelNode());
        assertNotRejectedOperation(node, node);
    }

    @Test
    public void testDefaultOperationOneInChain() throws Exception {

        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V2_0_0);
        chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr1")
                .setValueConverter(new SimpleAttributeConverter("old", "new"), "attr2");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = new ModelNode();
        original.get(OP_ADDR).set(address.toModelNode());
        original.get(OP).set(ADD);
        original.get("attr1").set(new ValueExpression("${no.no}"));
        original.get("attr2").set("old");
        original.get("attr3").set("whatever");

        OperationTransformer.TransformedOperation op =  transformOperation(original, ModelVersion.create(1));
        Assert.assertTrue(op.rejectOperation(success()));
        ModelNode expected = original.clone();
        expected.get("attr2").set("new");
        assertRejectedOperation(original, expected);

        ModelNode originalNotRejected = original.clone();
        originalNotRejected.get("attr1").set("accepted");
        expected = originalNotRejected.clone();
        expected.get("attr2").set("new");
        assertNotRejectedOperation(originalNotRejected, expected);
    }

    @Test
    public void testDefaultOperationChain() throws Exception {

        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        chainedBuilder.createBuilder(V4_0_0, V3_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr1")
                .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr");
        chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr2")
                .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr");
        chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr3")
                .setValueConverter(new SimpleAttributeConverter("two", "three"), "attr");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = new ModelNode();
        original.get(OP_ADDR).set(address.toModelNode());
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr2").set("b");
        original.get("attr3").set("c");
        original.get("attr").set("old");

        ModelNode expected = original.clone();
        expected.get("attr").set("three");
        assertNotRejectedOperation(original, expected);

        ModelNode reject = original.clone();
        reject.get("attr1").set(new ValueExpression("${reject}"));
        expected = reject.clone();
        expected.get("attr").set("three");
        assertRejectedOperation(reject, expected);

        reject = original.clone();
        reject.get("attr2").set(new ValueExpression("${reject}"));
        expected = reject.clone();
        expected.get("attr").set("three");
        assertRejectedOperation(reject, expected);

        reject = original.clone();
        reject.get("attr3").set(new ValueExpression("${reject}"));
        expected = reject.clone();
        expected.get("attr").set("three");
        assertRejectedOperation(reject, expected);
    }

    private void assertRejectedOperation(ModelNode reject, ModelNode expected) throws OperationFailedException {
        OperationTransformer.TransformedOperation op;
        for (ModelVersion version : VALID_TESTED_VERSIONS) {
            op = transformOperation(reject, version);
            Assert.assertTrue(op.rejectOperation(success()));
            Assert.assertEquals(expected, op.getTransformedOperation());
        }
        op = transformOperation(reject, UNKNOWN_VERSION);
        Assert.assertFalse(op.rejectOperation(success()));
        Assert.assertEquals(reject, op.getTransformedOperation());
    }

    private void assertNotRejectedOperation(ModelNode original, ModelNode expected) throws OperationFailedException {
        OperationTransformer.TransformedOperation op;
        for (ModelVersion version : VALID_TESTED_VERSIONS) {
            op = transformOperation(original, version);
            Assert.assertFalse(op.rejectOperation(success()));
            Assert.assertEquals(expected, op.getTransformedOperation());
        }
        op = transformOperation(original, UNKNOWN_VERSION);
        Assert.assertFalse(op.rejectOperation(success()));
        Assert.assertEquals(original, op.getTransformedOperation());
    }

    @Test
    public void testDefaultOperationChildWithNoTransformer() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        chainedBuilder.createBuilder(V4_0_0, V3_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr1")
                .setValueConverter(new SimpleAttributeConverter("old", "new"), "attr2");
        chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr1")
                .setValueConverter(new SimpleAttributeConverter("old", "new"), "attr2");
        chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .addRejectCheck(RejectAttributeChecker.SIMPLE_EXPRESSIONS, "attr1")
                .setValueConverter(new SimpleAttributeConverter("old", "new"), "attr2");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH).append("one", "two");
        ModelNode original = new ModelNode();
        original.get(OP_ADDR).set(address.toModelNode());
        original.get(OP).set(ADD);
        original.get("attr1").set(new ValueExpression("${no.no}"));
        original.get("attr2").set("old");
        original.get("attr3").set("whatever");

        //Since this operation is on child resource with no transformers registered we should
        //just get the original back and there should be no rejections
        assertNotRejectedOperation(original, original);
    }


    @Test
    public void testOperationAgainstDiscardedAndRejectedResources() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0)
            .getAttributeBuilder()
                .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr")
                .end();
        builder.discardChildResource(PathElement.pathElement("child", "discard1"));
        builder.rejectChildResource(PathElement.pathElement("child", "reject1"));

        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr")
                .end();
        builder.discardChildResource(PathElement.pathElement("child", "discard2"));
        builder.rejectChildResource(PathElement.pathElement("child", "reject2"));

        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .setValueConverter(new SimpleAttributeConverter("two", "three"), "attr")
                .end();
        builder.discardChildResource(PathElement.pathElement("child", "discard3"));
        builder.rejectChildResource(PathElement.pathElement("child", "reject3"));

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that the root operations get transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        OperationTransformer.TransformedOperation op =  transformOperation(original, ModelVersion.create(1));
        ModelNode expected = original.clone();
        expected.get("attr").set("three");
        assertNotRejectedOperation(original, expected);

        //Test ops against children with nothing special registered, and its children
        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "normal")));
        original.get("attr1").set("a");
        assertNotRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "normal")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertNotRejectedOperation(original, original);

        //Test ops against the discarded resources
        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard1")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard2")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard3")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        //Test ops against children of the discarded resources
        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard1")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard2")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "discard3")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertDiscardedOperation(original);

        //Test ops against the rejected resources
        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject1")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject2")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject3")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);

        //Test ops against children of the rejected resources
        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject1")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject2")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(PathElement.pathElement("child", "reject3")).append(PathElement.pathElement("one", "more")));
        original.get("attr1").set("a");
        assertRejectedOperation(original, original);
    }

    @Test
    public void testRawOperationOverride() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0)
            .addRawOperationTransformationOverride("test", new OperationTransformer() {
                @Override
                public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
                        throws OperationFailedException {
                    Assert.assertEquals("test", operation.get(OP).asString());
                    return new TransformedOperation(Util.createOperation("test1", address), TransformedOperation.ORIGINAL_RESULT);
                }
            });
        builder.getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr")
            .end();


        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0)
                .addRawOperationTransformationOverride("test1", new OperationTransformer() {
                    @Override
                    public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
                            throws OperationFailedException {
                        Assert.assertEquals("test1", operation.get(OP).asString());
                        return new TransformedOperation(Util.createOperation("test2", address), TransformedOperation.ORIGINAL_RESULT);
                    }
                });
        builder.getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr")
            .end();

        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0)
                .addRawOperationTransformationOverride("test2", new OperationTransformer() {
                    @Override
                    public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
                            throws OperationFailedException {
                        Assert.assertEquals("test2", operation.get(OP).asString());
                        return new TransformedOperation(Util.createOperation("test3", address), TransformedOperation.ORIGINAL_RESULT);
                    }
                });
        builder.getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("two", "three"), "attr")
            .end();

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that not overridded operations get transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        ModelNode expected = original.clone();
        expected.get("attr").set("three");
        assertNotRejectedOperation(original, expected);

        //Test that the raw operation transformer kicks in
        original = Util.createEmptyOperation("test", address);
        for(ModelVersion version : VALID_TESTED_VERSIONS) {
            transformOperation(original, version);
        }
    }

    @Test
    public void testDiscardOperations() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        chainedBuilder.createBuilder(V4_0_0, V3_0_0)
                .discardOperations("discard1A", "discard1B");
        chainedBuilder.createBuilder(V3_0_0, V2_0_0)
                .discardOperations("discard2A", "discard2B");
        chainedBuilder.createBuilder(V2_0_0, V1_0_0)
                .discardOperations("discard3A", "discard3B");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that not discarded operations get transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        assertNotRejectedOperation(original, original);

        //Now test that the discarded operations get discarded
        original = Util.createEmptyOperation("discard1A", address);
        assertDiscardedOperation(original);

        original = Util.createEmptyOperation("discard1B", address);
        assertDiscardedOperation(original);

        original = Util.createEmptyOperation("discard2A", address);
        assertDiscardedOperation(original);

        original = Util.createEmptyOperation("discard2B", address);
        assertDiscardedOperation(original);

        original = Util.createEmptyOperation("discard3A", address);
        assertDiscardedOperation(original);

        original = Util.createEmptyOperation("discard3B", address);
        assertDiscardedOperation(original);
    }

    private void assertDiscardedOperation(ModelNode original) throws OperationFailedException {
        OperationTransformer.TransformedOperation op;
        for (ModelVersion version : VALID_TESTED_VERSIONS) {
            op = transformOperation(original, version);
            Assert.assertFalse(op.rejectOperation(success()));
            Assert.assertNull(op.getTransformedOperation());
        }
        op = transformOperation(original, UNKNOWN_VERSION);
        Assert.assertFalse(op.rejectOperation(success()));
        Assert.assertEquals(original, op.getTransformedOperation());
    }

    @Test
    public void testOperationTransformationOverride() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0)
                .getAttributeBuilder()
                    .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr")
                    .end();
        builder.addOperationTransformationOverride("test")
                .inheritResourceAttributeDefinitions()
                .addRename("attr1", "attr-one")
                .rename("testA");

        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr")
                .end();
        builder.addOperationTransformationOverride("testA")
            .setValueConverter(new NewAttributeConverter("new-one"), "attr-new")
            .setValueConverter(new SimpleAttributeConverter("a", "No1"), "attr-one");


        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .setValueConverter(new NewAttributeConverter("newA"), "new-main")
                .end();
        builder.addOperationTransformationOverride("testA")
            .inheritResourceAttributeDefinitions()
            .setValueConverter(new SimpleAttributeConverter("new-one", "new1"), "attr-new")
            .addRename("attr-new", "new-attr")
            .rename("testB");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that the main operation gets transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        ModelNode expected = original.clone();
        expected.get("attr").set("two");
        expected.get("new-main").set("newA");
        assertNotRejectedOperation(original, expected);

        //Test the overridden operation
        original = Util.createAddOperation(address);
        original.get(OP).set("test");
        original.get("attr").set("old");
        original.get("attr1").set("a");

        expected = Util.createOperation("testB", address);
        expected.get("attr-one").set("No1");
        expected.get("attr").set("one");
        expected.get("new-main").set("newA");
        expected.get("new-attr").set("new1");
        assertNotRejectedOperation(original, expected);
    }

    @Test
    public void testCustomOperationTransformationOverride() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0)
                .getAttributeBuilder()
                    .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr")
                    .end();
        builder.addOperationTransformationOverride("test")
                .inheritResourceAttributeDefinitions()
                .addRename("attr1", "attr-one")
                .rename("testA")
                .setCustomOperationTransformer(new OperationTransformer() {
                    @Override
                    public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
                            throws OperationFailedException {
                        Assert.assertEquals("testA", operation.get(OP).asString());
                        Assert.assertEquals("one", operation.get("attr").asString());
                        Assert.assertEquals("a", operation.get("attr-one").asString());

                        ModelNode copy = operation.clone();
                        copy.get("attr").set("one_");
                        copy.get("attr-one").set("a_");
                        return new TransformedOperation(copy, TransformedOperation.ORIGINAL_RESULT);
                    }
                });


        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr")
                .end();
        builder.addOperationTransformationOverride("testA")
            .setValueConverter(new SimpleAttributeConverter("a_", "No1"), "attr-one")
            .setCustomOperationTransformer(new OperationTransformer() {
                @Override
                public TransformedOperation transformOperation(TransformationContext context, PathAddress address, ModelNode operation)
                        throws OperationFailedException {
                    ModelNode copy = operation.clone();
                    copy.get("attr-new").set("new-one");
                    return new TransformedOperation(copy, TransformedOperation.ORIGINAL_RESULT);
                }
            });


        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .setValueConverter(new NewAttributeConverter("newA"), "new-main")
                .end();
        builder.addOperationTransformationOverride("testA")
            .inheritResourceAttributeDefinitions()
            .setValueConverter(new SimpleAttributeConverter("new-one", "new1"), "attr-new")
            .addRename("attr-new", "new-attr")
            .rename("testB");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that the main operation gets transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        ModelNode expected = original.clone();
        expected.get("attr").set("two");
        expected.get("new-main").set("newA");
        assertNotRejectedOperation(original, expected);

        //Test the overridden operation
        original = Util.createAddOperation(address);
        original.get(OP).set("test");
        original.get("attr").set("old");
        original.get("attr1").set("a");

        expected = Util.createOperation("testB", address);
        expected.get("attr-one").set("No1");
        expected.get("attr").set("one_");
        expected.get("new-main").set("newA");
        expected.get("new-attr").set("new1");
        assertNotRejectedOperation(original, expected);

    }

    @Test
    public void testOperationsTransformationHierarchy() throws Exception {
        final PathElement common = PathElement.pathElement("child", "common");
        final PathElement one = PathElement.pathElement("child", "one");
        final PathElement two = PathElement.pathElement("child", "two");
        final PathElement three = PathElement.pathElement("child", "three");
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0);
        builder.addChildResource(common).getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("old", "one"), "attr")
            .end();
        builder.addChildResource(one).addOperationTransformationOverride("test")
            .setValueConverter(new NewAttributeConverter("a"), "attr-1");

        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0);
        builder.addChildResource(common).getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("one", "two"), "attr");
        builder.addChildResource(two).addOperationTransformationOverride("test")
            .setValueConverter(new NewAttributeConverter("b"), "attr-2");

        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0);
        builder.addChildResource(common).getAttributeBuilder()
            .setValueConverter(new SimpleAttributeConverter("two", "three"), "attr");
        builder.addChildResource(three).addOperationTransformationOverride("test")
            .setValueConverter(new NewAttributeConverter("c"), "attr-3");


        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that the root operation gets (not) transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get("attr").set("old");
        assertNotRejectedOperation(original, original);

        //Add ops on the common child should have the 'common' transformation
        original = Util.createAddOperation(address.append(common));
        original.get("attr").set("old");
        ModelNode expected = original.clone();
        expected.get("attr").set("three");
        assertNotRejectedOperation(original, expected);

        //Add ops on the one, two and three children should not be transformed
        original = Util.createAddOperation(address.append(one));
        assertNotRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(two));
        assertNotRejectedOperation(original, original);

        original = Util.createAddOperation(address.append(three));
        assertNotRejectedOperation(original, original);

        //The overridden ops on the children should have basic transformation
        original = Util.createEmptyOperation("test", address.append(one));
        expected = original.clone();
        expected.get("attr-1").set("a");
        assertNotRejectedOperation(original, expected);

        original = Util.createEmptyOperation("test", address.append(two));
        expected = original.clone();
        expected.get("attr-2").set("b");
        assertNotRejectedOperation(original, expected);

        original = Util.createEmptyOperation("test", address.append(three));
        expected = original.clone();
        expected.get("attr-3").set("c");
        assertNotRejectedOperation(original, expected);
    }


    @Test
    public void testOperationsTransformationHierarchyWithPathChange() throws Exception {
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);


        ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(V4_0_0, V3_0_0);
        builder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("old", "one"), "attr");
        ResourceTransformationDescriptionBuilder childBuilder =
                builder.addChildRedirection(PathElement.pathElement("a", "one"), PathElement.pathElement("a1", "one1"));
        childBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("original", "ONE"), "ch");
        ResourceTransformationDescriptionBuilder grandChildBuilder =
                childBuilder.addChildRedirection(PathElement.pathElement("x", "a"), PathElement.pathElement("x1", "a1"));
        grandChildBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("start", "A"), "gc");
        grandChildBuilder = builder.addChildResource(PathElement.pathElement("b", "two")).addChildRedirection(PathElement.pathElement("y", "b"), PathElement.pathElement("y1", "b1"));
        grandChildBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("first", "second"), "gc");

        builder = chainedBuilder.createBuilder(V3_0_0, V2_0_0);
        builder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("one", "two"), "attr");
        childBuilder =
                builder.addChildRedirection(PathElement.pathElement("a1", "one1"), PathElement.pathElement("a2", "one2"));
        childBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("ONE", "TWO"), "ch");
        grandChildBuilder = builder.addChildResource(PathElement.pathElement("b", "two")).addChildRedirection(PathElement.pathElement("y1", "b1"), PathElement.pathElement("y2", "b2"));
        grandChildBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("second", "third"), "gc");

        builder = chainedBuilder.createBuilder(V2_0_0, V1_0_0);
        builder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("two", "three"), "attr");
        childBuilder =
                builder.addChildRedirection(PathElement.pathElement("a2", "one2"), PathElement.pathElement("a3", "one3"));
        childBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("TWO", "THREE"), "ch");
        grandChildBuilder =
                childBuilder.addChildRedirection(PathElement.pathElement("x1", "a1"), PathElement.pathElement("x2", "a2"));
        grandChildBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("A", "B"), "gc");
        grandChildBuilder = builder.addChildResource(PathElement.pathElement("b", "two")).addChildRedirection(PathElement.pathElement("y2", "b2"), PathElement.pathElement("y3", "b3"));
        grandChildBuilder.getAttributeBuilder().setValueConverter(new SimpleAttributeConverter("third", "fourth"), "gc");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        //Although tested elsewhere test that the root operation gets transformed as expected
        final PathAddress address = PathAddress.EMPTY_ADDRESS.append(PATH);
        ModelNode original = Util.createAddOperation(address);
        original.get(OP).set(ADD);
        original.get("attr1").set("a");
        original.get("attr").set("old");

        ModelNode expected = original.clone();
        expected.get("attr").set("three");
        assertNotRejectedOperation(original, expected);

        //Test child redirection
        original = Util.createAddOperation(address.append(PathElement.pathElement("a", "one")));
        original.get("ch").set("original");
        expected = Util.createAddOperation(address.append(PathElement.pathElement("a3", "one3")));
        expected.get("ch").set("THREE");
        assertNotRejectedOperation(original, expected);

        //Test grandchild redirection when the intermediate child changes
        original = Util.createAddOperation(address.append(PathElement.pathElement("a", "one"), PathElement.pathElement("x", "a")));
        original.get("gc").set("start");
        expected = Util.createAddOperation(address.append(PathElement.pathElement("a3", "one3"), PathElement.pathElement("x2", "a2")));
        expected.get("gc").set("B");
        assertNotRejectedOperation(original, expected);

        //Test grandchild redirection when the intermediate child stays the same
        original = Util.createAddOperation(address.append(PathElement.pathElement("b", "two"), PathElement.pathElement("y", "b")));
        original.get("gc").set("first");
        expected = Util.createAddOperation(address.append(PathElement.pathElement("b", "two"), PathElement.pathElement("y3", "b3")));
        expected.get("gc").set("fourth");
        assertNotRejectedOperation(original, expected);

        /*
        This does not work, but should not be an issue since in the real world operations must be executed against the current model, so a1=one1 does not exist

        //Test that ops for addresses further down the chain don't get transformed
        original = Util.createAddOperation(address.append(PathElement.pathElement("a1", "one1")));
        op =  transformOperation(original, ModelVersion.create(1));
        Assert.assertFalse(op.rejectOperation(success()));
        Assert.assertEquals(original, op.getTransformedOperation());
         */
    }


    @Test
    public void testReadResourceFromTransformer() throws Exception {
        resourceModel.get("attr1").set("reject1");
        //Note that when reading the resource from the chained transformers, the original model is used.
        ChainedTransformationDescriptionBuilder chainedBuilder = TransformationDescriptionBuilder.Factory.createChainedInstance(PATH, V4_0_0);
        chainedBuilder.createBuilder(V4_0_0, V3_0_0)
            .getAttributeBuilder()
                .addRejectCheck(new SimpleRejectAttributeChecker("reject1"), "attr1");
        chainedBuilder.createBuilder(V3_0_0, V2_0_0)
            .getAttributeBuilder()
                .addRejectCheck(new SimpleRejectAttributeChecker("reject2"), "attr1");
        chainedBuilder.createBuilder(V2_0_0, V1_0_0)
            .getAttributeBuilder()
                .addRejectCheck(new SimpleRejectAttributeChecker("reject3"), "attr1");

        TransformationDescription.Tools.register(chainedBuilder.build(V1_0_0, V2_0_0, V3_0_0).get(V1_0_0), transformersSubRegistration);

        final PathAddress address = PathAddress.pathAddress(PATH);
        ModelNode original = new ModelNode();
        original.get(OP_ADDR).set(address.toModelNode());
        original.get(OP).set(ADD);

        TransformedOperation op =  transformOperation(original, ModelVersion.create(1));
        Assert.assertTrue(op.rejectOperation(success()));

        resourceModel.get("attr1").set("reject2");
        op =  transformOperation(original, ModelVersion.create(1));
        Assert.assertTrue(op.rejectOperation(success()));

        resourceModel.get("attr1").set("reject3");
        op =  transformOperation(original, ModelVersion.create(1));
        Assert.assertTrue(op.rejectOperation(success()));

        resourceModel.get("attr1").set("ok");
        assertNotRejectedOperation(original, original);
    }

    private OperationTransformer.TransformedOperation transformOperation(final ModelNode operation, final ModelVersion version) throws OperationFailedException {
        final TransformationTarget target = create(registry, version);
        final TransformationContext context = createContext(target);
        return getTransfomers(target).transformOperation(context, operation.clone());
    }

    private ResourceTransformationContext createContext(final TransformationTarget target) {
        return Transformers.Factory.create(target, resourceRoot, resourceRegistration, ExpressionResolver.TEST_RESOLVER,
                RunningMode.NORMAL, ProcessType.STANDALONE_SERVER, null);
    }

    private Transformers getTransfomers(final TransformationTarget target) {
        return Transformers.Factory.create(target);
    }

    protected TransformationTarget create(final TransformerRegistry registry, ModelVersion version) {
        return create(registry, version, TransformationTarget.TransformationTargetType.SERVER);
    }

    protected TransformationTarget create(final TransformerRegistry registry, ModelVersion version, TransformationTarget.TransformationTargetType type) {
        return TransformationTargetImpl.create(null, registry, version, Collections.<PathAddress, ModelVersion>emptyMap(), type);
    }

    private static ModelNode success() {
        final ModelNode result = new ModelNode();
        result.get(ModelDescriptionConstants.OUTCOME).set(ModelDescriptionConstants.SUCCESS);
        result.get(ModelDescriptionConstants.RESULT);
        return result;
    }

    private static class SimpleAttributeConverter extends AttributeConverter.DefaultAttributeConverter {
        private final ModelNode expectedValue;
        private final ModelNode newValue;

        SimpleAttributeConverter(String expectedValue, String newValue) {
            this.expectedValue = new ModelNode(expectedValue);
            this.newValue = new ModelNode(newValue);
        }

        SimpleAttributeConverter(ModelNode expectedValue, ModelNode newValue) {
            this.expectedValue = expectedValue;
            this.newValue = newValue;
        }

        @Override
        protected void convertAttribute(PathAddress address, String attributeName, ModelNode attributeValue, TransformationContext context) {
            //System.out.println("-----> transform " + address + " " + attributeValue.asString() + " expected:" + expectedValue);
            Assert.assertEquals(expectedValue, attributeValue);
            attributeValue.set(newValue);
        }
    }

    private static class NewAttributeConverter extends SimpleAttributeConverter {
        NewAttributeConverter(String newValue) {
            super(new ModelNode(), new ModelNode(newValue));
        }
    }

    private static class SimpleRejectAttributeChecker implements RejectAttributeChecker {
        private final ModelNode value;
        public SimpleRejectAttributeChecker(String value) {
            this.value = new ModelNode(value);
        }

        @Override
        public boolean rejectOperationParameter(PathAddress address, String attributeName, ModelNode attributeValue,
                ModelNode operation, TransformationContext context) {
            boolean relative = context.readResource(PathAddress.EMPTY_ADDRESS).getModel().get(attributeName).equals(value);
            boolean absolute = context.readResourceFromRoot(address).getModel().get(attributeName).equals(value);
            Assert.assertEquals(relative, absolute);
            return relative;
        }

        @Override
        public boolean rejectResourceAttribute(PathAddress address, String attributeName, ModelNode attributeValue,
                TransformationContext context) {
            return false;
        }

        @Override
        public String getRejectionLogMessageId() {
            return "Test123";
        }

        @Override
        public String getRejectionLogMessage(Map<String, ModelNode> attributes) {
            return "Failed!!!!";
        }

    }
}
