/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.as.test.shared;

import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.FAILURE_DESCRIPTION;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.HOST;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.OUTCOME;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.RESULT;
import static org.jboss.as.controller.descriptions.ModelDescriptionConstants.SUCCESS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Set;

import org.jboss.as.controller.ControlledProcessState;
import org.jboss.as.server.ServerEnvironment;
import org.jboss.as.test.integration.management.util.CLIOpResult;
import org.jboss.as.test.integration.management.util.CLIWrapper;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;
import org.junit.Assert;

/**
 *
 * @author <a href="mailto:ehugonne@redhat.com">Emmanuel Hugonnet</a> (c) 2014 Red Hat, inc.
 */
public class ModelParserUtils {
    // Timeout for the embedded Server / Host Controller to fully start, in seconds.
    private static final long EMBEDDED_FULLY_STARTED_TIMEOUT = TimeoutUtil.adjust(5*60);

    /**
     * Tests the ability to boot an admin-only server using the given config, persist the config,
     * reload it, and confirm that the configuration model from the original boot matches the model
     * from the reload.
     *
     * @param originalConfig the config file to use
     * @param jbossHome      directory to use as $JBOSS_HOME
     * @return the configuration model read after the reload
     * @throws Exception if an error occurs
     */
    public static ModelNode standaloneXmlTest(File originalConfig, File jbossHome) throws Exception {
        File serverDir =  new File(jbossHome, "standalone");

        File configCopy = new File(serverDir, "configuration" + File.separatorChar + originalConfig.getName());
        FileUtils.copyFile(originalConfig, configCopy);

        CLIWrapper cli = new CLIWrapper(false);
        try {

            String line = "embed-server --admin-only=true --server-config=" + originalConfig.getName() + " --std-out=echo --jboss-home=" + jbossHome.getCanonicalPath() + " --timeout=" + EMBEDDED_FULLY_STARTED_TIMEOUT;
            cli.sendLine(line);
            assertProcessState(cli, ControlledProcessState.State.RUNNING.toString(), TimeoutUtil.adjust(30000), false);
            ModelNode firstResult = readResourceTree(cli);
            cli.sendLine("/system-property=model-parser-util:add(value=true)");
            cli.sendLine("/system-property=model-parser-util:remove");
            cli.sendLine("reload --admin-only=true");
            assertProcessState(cli, ControlledProcessState.State.RUNNING.toString(), TimeoutUtil.adjust(30000), false);
            ModelNode secondResult = readResourceTree(cli);
            compare(firstResult, secondResult);
            return secondResult;

        } finally {
            try {
                cli.quit();
            } finally {
                System.clearProperty(ServerEnvironment.SERVER_BASE_DIR);
            }
        }
    }

    /**
     * Tests the ability to boot an admin-only Host Controller using the given host config, persist the config,
     * reload it, and confirm that the configuration model from the original boot matches the model
     * from the reload.
     *
     * @param originalConfig the config file to use for the host model
     * @param jbossHome      directory to use as $JBOSS_HOME
     * @return the host subtree from the configuration model read after the reload
     * @throws Exception if an error occurs
     */
    public static ModelNode hostXmlTest(final File originalConfig, File jbossHome) throws Exception {
        return hostControllerTest(originalConfig, jbossHome, true);
    }

    private static ModelNode hostControllerTest(final File originalConfig, final File target, boolean hostXml) throws Exception {
        File domainDir =  new File(target, "domain");

        File configCopy = new File(domainDir, "configuration" + File.separatorChar + originalConfig.getName());
        FileUtils.copyFile(originalConfig, configCopy);

        CLIWrapper cli = new CLIWrapper(false);
        try {
            String configType = hostXml ? "--host-config=" : "--domain-config=";
            String line = "embed-host-controller " + configType + originalConfig.getName() + " --std-out=echo --jboss-home=" + target.getCanonicalPath()  + " --timeout=" + EMBEDDED_FULLY_STARTED_TIMEOUT;
            cli.sendLine(line);
            assertProcessState(cli, ControlledProcessState.State.RUNNING.toString(), TimeoutUtil.adjust(30000), true);
            ModelNode firstResult = readResourceTree(cli);
            String hostName = firstResult.get(HOST).asProperty().getName();
            cli.sendLine("/system-property=model-parser-util:add(value=true)");
            cli.sendLine("/system-property=model-parser-util:remove");
            cli.sendLine("reload --host=" + hostName + " --admin-only=true");
            assertProcessState(cli, ControlledProcessState.State.RUNNING.toString(), TimeoutUtil.adjust(30000), true);
            ModelNode secondResult = readResourceTree(cli);
            compare(pruneDomainModel(firstResult, hostXml), pruneDomainModel(secondResult, hostXml));
            return secondResult;

        } finally {
            try {
                cli.quit();
            } finally {
                System.clearProperty(ServerEnvironment.SERVER_BASE_DIR);
            }
        }
    }

    private static ModelNode pruneDomainModel(ModelNode model, boolean forHost) {
        ModelNode result = new ModelNode();
        if (forHost) {
            result.get(HOST).set(model.get(HOST));
        } else {
            for (Property prop : model.asPropertyList()) {
                if (!HOST.equals(prop.getName())) {
                    result.get(prop.getName()).set(prop.getValue());
                }
            }
        }
        return result;
    }


    /**
     * Tests the ability to boot an admin-only Host Controller using the given domain config, persist the config,
     * reload it, and confirm that the configuration model from the original boot matches the model
     * from the reload.
     *
     * @param originalConfig the config file to use for the domain model
     * @param jbossHome      directory to use as $JBOSS_HOME
     * @return the configuration model read after the reload, excluding the host subtree
     * @throws Exception if an error occurs
     */
    public static ModelNode domainXmlTest(File originalConfig, File jbossHome) throws Exception {
        return hostControllerTest(originalConfig, jbossHome, false);
    }

    private static void assertProcessState(CLIWrapper cli, String expected, int timeout, boolean forHost) throws InterruptedException {
        long done = timeout < 1 ? 0 : System.currentTimeMillis() + timeout;
        StringBuilder historyBuf = new StringBuilder();
        String state = null;
        do {
            try {
                state = forHost ? getHostState(cli) : getServerState(cli);
                historyBuf.append(state).append("\n");
            } catch (Exception ignored) {
                historyBuf.append(ignored).append("--").append(cli.readOutput()).append("\n");
            }
            if (expected.equals(state)) {
                return;
            } else {
                Thread.sleep(20);
            }
        } while (timeout > 0 && System.currentTimeMillis() < done);
        assertEquals(historyBuf.toString(), expected, state);
    }

    private static String getServerState(CLIWrapper cli) throws IOException {
        cli.sendLine(":read-attribute(name=server-state)", true);
        CLIOpResult result = cli.readAllAsOpResult();
        ModelNode resp = result.getResponseNode();
        ModelNode stateNode = result.isIsOutcomeSuccess() ? resp.get(RESULT) : resp.get(FAILURE_DESCRIPTION);
        return stateNode.asString();
    }

    private static String getHostState(CLIWrapper cli) throws IOException {
        cli.sendLine("/host=*:read-attribute(name=host-state)", true);
        CLIOpResult result = cli.readAllAsOpResult();
        ModelNode resp = result.getResponseNode();
        ModelNode stateNode;
        if (result.isIsOutcomeSuccess()) {
            stateNode = resp.get(RESULT).get(0).get(RESULT);
        } else {
            stateNode = resp.get(FAILURE_DESCRIPTION);
        }
        return stateNode.asString();
    }

    private static ModelNode readResourceTree(CLIWrapper cli) {
        cli.sendLine("/:read-resource(recursive=true)");
        ModelNode response = ModelNode.fromString(cli.readOutput());
        assertEquals(response.toString(), SUCCESS, response.get(OUTCOME).asString());
        ModelNode firstResult = response.get(RESULT);
        assertTrue(response.toString(), firstResult.isDefined());
        return firstResult;
    }

    private static void compare(ModelNode node1, ModelNode node2) {
        Assert.assertEquals(node1.getType(), node2.getType());
        if (node1.getType() == ModelType.OBJECT) {
            final Set<String> keys1 = node1.keys();
            final Set<String> keys2 = node2.keys();
            Assert.assertEquals(node1 + "\n" + node2, keys1.size(), keys2.size());

            for (String key : keys1) {
                final ModelNode child1 = node1.get(key);
                Assert.assertTrue("Missing: " + key + "\n" + node1 + "\n" + node2, node2.has(key));
                final ModelNode child2 = node2.get(key);
                if (child1.isDefined()) {
                    Assert.assertTrue(child1.toString(), child2.isDefined());
                    compare(child1, child2);
                } else {
                    Assert.assertFalse(child2.asString(), child2.isDefined());
                }
            }
        } else if (node1.getType() == ModelType.LIST) {
            List<ModelNode> list1 = node1.asList();
            List<ModelNode> list2 = node2.asList();
            Assert.assertEquals(list1 + "\n" + list2, list1.size(), list2.size());

            for (int i = 0; i < list1.size(); i++) {
                compare(list1.get(i), list2.get(i));
            }

        } else if (node1.getType() == ModelType.PROPERTY) {
            Property prop1 = node1.asProperty();
            Property prop2 = node2.asProperty();
            Assert.assertEquals(prop1 + "\n" + prop2, prop1.getName(), prop2.getName());
            compare(prop1.getValue(), prop2.getValue());

        } else {
            Assert.assertEquals("\n\"" + node1.asString() + "\"\n\"" + node2.asString() + "\"\n-----", node1.asString().trim(), node2.asString().trim());
        }
    }

}
