/* ###
 * IP: GHIDRA
 * EXCLUDE: YES
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package ghidra.app.plugin.core.calltree;

import static org.junit.Assert.*;

import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JTree;
import javax.swing.tree.TreePath;

import org.junit.*;

import docking.ActionContext;
import docking.action.*;
import docking.widgets.tree.*;
import generic.test.AbstractGenericTest;
import ghidra.app.cmd.data.CreateDataCmd;
import ghidra.app.cmd.function.CreateExternalFunctionCmd;
import ghidra.app.cmd.function.SetFunctionNameCmd;
import ghidra.app.cmd.refs.AddMemRefCmd;
import ghidra.app.cmd.refs.SetExternalRefCmd;
import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin;
import ghidra.app.plugin.core.gotoquery.GoToServicePlugin;
import ghidra.app.services.GoToService;
import ghidra.app.services.ProgramManager;
import ghidra.framework.plugintool.PluginTool;
import ghidra.program.database.ProgramBuilder;
import ghidra.program.database.ProgramDB;
import ghidra.program.model.address.Address;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.PointerDataType;
import ghidra.program.model.listing.Function;
import ghidra.program.model.listing.FunctionManager;
import ghidra.program.model.symbol.*;
import ghidra.program.util.ProgramLocation;
import ghidra.program.util.ProgramSelection;
import ghidra.test.AbstractGhidraHeadedIntegrationTest;
import ghidra.test.TestEnv;

public class CallTreePluginTest extends AbstractGhidraHeadedIntegrationTest {

	private TestEnv env;
	private CodeBrowserPlugin codeBrowserPlugin;
	private PluginTool tool;
	private CallTreePlugin callTreePlugin;
	private CallTreeProvider provider;

	private GTree incomingTree;
	private GTree outgoingTree;

	private ProgramBuilder builder;
	private ProgramDB program;

	private GoToService goToService;

	@Before
	public void setUp() throws Exception {
		env = new TestEnv();
		tool = env.getTool();

		tool.addPlugin(CodeBrowserPlugin.class.getName());
		tool.addPlugin(CallTreePlugin.class.getName());

		callTreePlugin = env.getPlugin(CallTreePlugin.class);

		GoToServicePlugin goToPlugin = env.getPlugin(GoToServicePlugin.class);
		goToService = (GoToService) invokeInstanceMethod("getGotoService", goToPlugin);
		codeBrowserPlugin = env.getPlugin(CodeBrowserPlugin.class);

		env.showTool();

		program = createProgram();
		ProgramManager pm = tool.getService(ProgramManager.class);
		pm.openProgram(program.getDomainFile());

		// setup a good start location
		goTo(addr("5000"));

		provider = callTreePlugin.getPrimaryProvider();
		tool.showComponentProvider(provider, true);
		incomingTree = (GTree) getInstanceField("incomingTree", provider);
		outgoingTree = (GTree) getInstanceField("outgoingTree", provider);
	}

	private ProgramDB createProgram() throws Exception {

		builder = new ProgramBuilder("Call Trees", ProgramBuilder._TOY);
		builder.createMemory(".text", "0x0", 0x11000);

		/*
		 	Create a function call tree that looks like:
		 	
		root 0000
		 	 a 1000
		 		 b 2000
		 			 c 3000
		 				 d 4000
		 					 <test function> e 5000
		 										 f 6000
		 											 g 7000
		 												 h 8000
		 													 i 9000
		 										 k 6100
		 											 l 7100
		 										 m 6000  (duplicate)
		 										     n 7000
		 
		 */

		//
		// 
		// -called by a chain of 5 other functions
		// -calls a chain of 4 other children
		// -has multiple children
		// -has duplicate children
		//
		function(0x0000, 0x1000);
		function(0x1000, 0x2000);
		function(0x2000, 0x3000);
		function(0x3000, 0x4000);
		function(0x4000, 0x5000);
		function(0x5000, 0x6000);
		function(0x5000, 0x6100);
		duplicateReference(0x5000, 0x6000);
		function(0x6000, 0x7000);
		function(0x6100, 0x7100);
		function(0x7000, 0x8000);
		function(0x8000, 0x9000);
		function(0x9000, 0x10000);

		return builder.getProgram();
	}

	private void duplicateReference(int from, int to) {
		// a bit of space so the function call is not at the entry point
		int offset = from + 10;
		while (!createReference(offset, to)) {
			offset++;
		}
	}

	private void function(int from, int to) throws Exception {
		ensureFunction(from);
		ensureFunction(to);

		int offset = from + 5;// a bit of space so the function call is not at the entry point
		while (!createReference(offset, to)) {
			offset++;
		}
	}

	private void ensureFunction(long from) throws Exception {
		ProgramDB p = builder.getProgram();
		FunctionManager fm = p.getFunctionManager();
		Function f = fm.getFunctionAt(addr(from));
		if (f != null) {
			return;
		}

		String a = Long.toHexString(from);
		builder.createEmptyFunction("Function_" + a, "0x" + a, 50, DataType.DEFAULT);
	}

	private boolean createReference(long from, long to) {
		ProgramDB p = builder.getProgram();
		ReferenceManager rm = p.getReferenceManager();
		Reference existing = rm.getReference(addr(from), addr(to), 0);
		if (existing != null) {
			return false;
		}

		builder.createMemoryCallReference("0x" + Long.toHexString(from),
			"0x" + Long.toHexString(to));
		return true;
	}

	private Address addr(long addr) {
		return builder.addr(addr);
	}

	@After
	public void tearDown() throws Exception {

		incomingTree.cancelWork();
		outgoingTree.cancelWork();

		env.dispose();
	}

	@Test
	public void testTextFilterIncoming() {
		setProviderFunction("0x5000");// has good depth for in and out

		int depth = 4;
		setDepth(depth);

		String existingCaller = "Function_1000";// four levels back
		setIncomingFilter(existingCaller);
		assertIncomingMaxDepth(depth, true);

		assertIncomingNode(existingCaller, depth);

		depth = 3;
		setDepth(depth);
		setIncomingFilter(existingCaller);
		assertIncomingMaxDepth(0, true);// filter no longer matches

		assertIncomingNoNode(existingCaller, depth, true);
	}

	@Test
	public void testTextFilterOutgoing() {
		setProviderFunction("0x5000");// has good depth for in and out

		int depth = 3;
		setDepth(depth);

		String existingCallee = "Function_8000";
		setOutgoingFilter(existingCallee);
		assertOutgoingMaxDepth(depth, true);

		assertOutgoingNode(existingCallee, depth);

		depth = 2;
		setDepth(depth);
		setOutgoingFilter(existingCallee);
		assertOutgoingMaxDepth(0, true);// filter no longer matches

		assertOutgoingNoNode(existingCallee, depth, true);
	}

	@Test
	public void testChangingDepthWillFilterOnNewText() {
		// 
		// Verifies that we can match a filter at one depth and then increase the depth and match
		// a new filtered item that is at a deeper level than the previous depth.
		//
		setProviderFunction("0x5000");// has good depth for in and out

		int depth = 3;
		setDepth(depth);

		String existingCaller = "2000";// at depth 3
		setIncomingFilter(existingCaller);
		assertIncomingMaxDepth(depth, true);

		assertIncomingNode(existingCaller, depth);

		setIncomingFilter("");
		depth = 4;
		setDepth(depth);
		existingCaller = "1000";// at depth 4
		setIncomingFilter(existingCaller);

		assertIncomingMaxDepth(depth, true);
		assertIncomingNode(existingCaller, depth);
	}

	@Test
	public void testDepthPersistence() {
		//
		// Set the depth and make sure it is passed to snapshot windows and such
		//
		int depth = 10;
		setDepth(depth);

		goTo(addr("0x5000"));// new function

		CallTreeProvider initialProvider = provider;
		CallTreeProvider newProvider = showProvider("6000");
		assertNotEquals(initialProvider, newProvider);
		assertEquals(depth, currentDepthSetting(newProvider));
	}

	@Test
	public void testShowingTransientProvider() {

		goTo(addr("0x5000"));// new function
		assertOnlyOneToolbarIcon(); // this was a regression

		CallTreeProvider initialProvider = provider;
		CallTreeProvider newProvider = showProvider("6000");
		assertNotEquals(initialProvider, newProvider);
		assertOnlyOneToolbarIcon();
	}

	private void assertOnlyOneToolbarIcon() {

		int toolBarActionCount = 0;
		Set<DockingActionIf> actions =
			getActionsByOwnerAndName(tool, callTreePlugin.getName(), provider.getName());
		for (DockingActionIf action : actions) {
			ToolBarData data = action.getToolBarData();
			if (data != null) {
				toolBarActionCount++;
			}
		}

		assertEquals("Expected exactly one toolbar action", 1, toolBarActionCount);
	}

	@Test
	public void testIncomingExpandToDepthFromRoot() {
		//
		// Select a node and expand it recursively, limiting it to the current recurse depth
		//
		setDepth(5);// restore default
		setProviderFunction("0x5000");

		String rootNodeAddress = "5000";
		GTreeNode node = selectIncomingNode(rootNodeAddress);

		fullyExpandIncomingNode(node);

		assertIncomingMaxDepth(currentDepthSetting(provider), false);
	}

	@Test
	public void testOutgoingExpandToDepthFromRoot() {
		//
		// Select a node and expand it recursively, limiting it to the current recurse depth
		//
		setDepth(5);// restore default
		setProviderFunction("0x5000");

		String rootNodeAddress = "5000";
		GTreeNode node = selectOutgoingNode(rootNodeAddress);

		fullyExpandOutgoingNode(node);

		assertOutgoingMaxDepth(currentDepthSetting(provider), false);
	}

	@Test
	public void testIncomingExpandToDepthSelectively() {
		//
		// Select a node and expand it recursively, limiting it to the current recurse depth
		// This is testing the expand action.
		//
		setProviderFunction("0x5000");// a function with multiple callers (incoming depth of 4)

		String rootChildAddress = "Function_4000";// // this node has at least 5 (default) depth
		GTreeNode node = selectIncomingNode(rootChildAddress);

		fullyExpandIncomingNode(node);

		int nodeDepth = node.getTreePath().getPathCount() - 1;// -1 for root node 
		int depth = 4 + nodeDepth;
		assertIncomingMaxDepth(depth, false);
		assertDepth(node, depth);
	}

	@Test
	public void testOutgoingExpandToDepthSelectively() {
		//
		// Select a node and expand it recursively, limiting it to the current recurse depth.
		// This is testing the expand action.
		//
		setProviderFunction("0x5000");// a function with multiple callers

		String rootChildAddress = "Function_6000";// this node has at least 5 (default) depth
		GTreeNode node = selectOutgoingNode(rootChildAddress);

		fullyExpandOutgoingNode(node);

		int depth = currentDepthSetting(provider);
		assertOutgoingMaxDepth(depth, false);
		assertDepth(node, depth);
	}

	@Test
	public void testFollowsNavigation() {
		//
		// Test that navigating in the code browser will update the provider when the cursor
		// is inside of a function.  This will only work when the 'follow incoming changes' action
		// is selected.
		//
		assertTrue(provider.isVisible());
		assertNotNull("Provider did not update its information when made visible",
			providerFunction());

		toggleFollowIncomingNavigation(false);

		assertProviderMatchesListingFunction();

		goTo(addr("0x6000"));
		assertNotEquals(
			"Provider's location should not have changed when not tracking location changes",
			getListingFunction(), providerFunction());

		toggleFollowIncomingNavigation(true);
		assertProviderMatchesListingFunction();

		goTo(addr("0x6100"));
		assertProviderMatchesListingFunction();
	}

	@Test
	public void testIncomingCalls() {
		//
		// Make sure there are some incoming calls.  Make sure we can open child nodes to see 
		// more incoming calls.
		//

		waitForTree(incomingTree);
		GTreeNode rootNode = getRootNode(incomingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Incoming tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		GTreeNode child0 = children.get(0);
		incomingTree.expandPath(child0);
		waitForTree(incomingTree);

		List<GTreeNode> grandChildren = child0.getChildren();
		assertTrue("Incoming tree child does not have callers as expected for child: " + child0,
			grandChildren.size() > 0);
	}

	@Test
	public void testOutgoingCalls() {
		setProviderFunction("0x5000");

		waitForTree(outgoingTree);
		GTreeNode rootNode = getRootNode(outgoingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		GTreeNode child1 = children.get(1);
		outgoingTree.expandPath(child1);
		waitForTree(outgoingTree);

		List<GTreeNode> grandChildren = child1.getChildren();
		assertTrue("Outgoing tree child does not have callers as expected for child: " + child1,
			grandChildren.size() > 0);
	}

	@Test
	public void testGoToFromNode() {
		setProviderFunction("0x5000");

		waitForTree(outgoingTree);
		GTreeNode rootNode = getRootNode(outgoingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		ProgramLocation originalLocation = currentLocation();

		GTreeNode child1 = children.get(1);// skip the first child--it is an external function
		outgoingTree.setSelectedNode(child1);
		waitForTree(outgoingTree);

		ActionContext actionContext = new ActionContext(provider, outgoingTree);
		DockingActionIf goToAction = getAction("Go To Destination");
		performAction(goToAction, actionContext, true);

		CallNode callNode = (CallNode) child1;
		ProgramLocation newCodeBrowserLocation = codeBrowserPlugin.getCurrentLocation();
		assertTrue("The Go To action did not navigate the code browser",
			!originalLocation.equals(newCodeBrowserLocation));
		assertEquals("The code browser did not navigate to the address of the selected node",
			callNode.getLocation().getAddress(), newCodeBrowserLocation.getAddress());
	}

	@Test
	public void testMakeSelectionFromNodes() {
		//
		// Test that the user can select a node and then right-click to make a program selection.
		//
		setProviderFunction("0x5000");

		DockingActionIf selectSourceAction = getAction("Select Call Source");
		ActionContext context = new ActionContext(provider, outgoingTree);

		assertTrue("The selection action was enabled when no node was selected",
			!selectSourceAction.isEnabledForContext(context));

		ProgramSelection currentSelection = codeBrowserPlugin.getCurrentSelection();
		assertTrue(currentSelection.isEmpty());

		waitForTree(outgoingTree);
		GTreeNode rootNode = getRootNode(outgoingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		OutgoingCallNode child0 = (OutgoingCallNode) children.get(0);
		clickNode(outgoingTree, child0);
		waitForTree(outgoingTree);

		assertTrue("The selection action was not enabled when a node was selected",
			selectSourceAction.isEnabledForContext(context));

		performAction(selectSourceAction, context, true);
		currentSelection = codeBrowserPlugin.getCurrentSelection();
		assertTrue(!currentSelection.isEmpty());

		Address callAddress = child0.getSourceAddress();
		assertEquals("Expected address not selected after performing action", callAddress,
			currentSelection.getMinAddress());
	}

	@Test
	public void testHomeAction_Navigation_DoNotFollow() {

		//
		// Test that the 'home' address does not change when the user navigates
		// 

		toggleFollowIncomingNavigation(false);
		Address startAddress = addr("0x5000");
		setProviderFunction("0x5000");

		DockingActionIf homeAction = getAction("Home");

		// go to some other address
		Address firstCallAddress = addr("0x6000");
		goTo(firstCallAddress);

		assertEquals(firstCallAddress, getListingAddress());

		performAction(homeAction, true);
		assertEquals(startAddress, getListingAddress());
	}

	@Test
	public void testHomeAction_Navigation_Follow() {

		//
		// Test that the 'home' address does not change when the user navigates
		// 

		toggleFollowIncomingNavigation(true);
		setProviderFunction("0x5000");

		DockingActionIf homeAction = getAction("Home");

		// go to some other address
		Address firstCallAddress = addr("0x6000");
		goTo(firstCallAddress);

		assertEquals(firstCallAddress, getListingAddress());

		performAction(homeAction, true);
		assertEquals(firstCallAddress, getListingAddress());
	}

	@Test
	public void testFilterOutgoingDuplicates() {
		//
		// Test that the filter action will remove duplicate entries from the child nodes of 
		// the outgoing tree
		//
		// setup a known state

		setProviderFunction("0x5000");

		final ToggleDockingAction filterDuplicatesAction =
			(ToggleDockingAction) getAction("Filter Duplicates");

		if (!filterDuplicatesAction.isSelected()) {
			performAction(filterDuplicatesAction, true);
		}

		waitForTree(outgoingTree);
		GTreeNode rootNode = getRootNode(outgoingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		// copy the names of the children into a map so that we can verify that there are 
		// no duplicates
		boolean shouldHaveDuplicates = false;
		Map<String, Integer> nameCountMap = createNameCountMap(rootNode);
		System.out.println("nameCountMap: before: = " + nameCountMap);
		assertDuplicateChildStatus(nameCountMap, shouldHaveDuplicates);

		performAction(filterDuplicatesAction, true);// deselect

		waitForTree(outgoingTree);

		rootNode = getRootNode(outgoingTree);
		nameCountMap = createNameCountMap(rootNode);
		shouldHaveDuplicates = true;
		System.out.println("nameCountMap: after: = " + nameCountMap);
		assertDuplicateChildStatus(nameCountMap, shouldHaveDuplicates);
	}

	@Test
	public void testTracksSelection() {
		//
		// Test the action that tracks node selection in the outgoing tree.  When toggled on, 
		// navigating the nodes in the outgoing tree should trigger a listing navigation.
		//
		Address startAddress = addr("0x5000");
		setProviderFunction("0x5000");

		// put the action in the correct state
		final ToggleDockingAction navigateAction =
			(ToggleDockingAction) getAction("Navigate Outgoing Nodes");

		if (!navigateAction.isSelected()) {
			performAction(navigateAction, true);
		}

		waitForTree(outgoingTree);
		GTreeNode rootNode = getRootNode(outgoingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		OutgoingCallNode child0 = (OutgoingCallNode) children.get(0);
		clickNode(outgoingTree, child0);
		waitForTree(outgoingTree);
		Address expectedAddress0 = child0.getSourceAddress();
		assertEquals("Node selection did not trigger navigation", getListingAddress(),
			expectedAddress0);

		OutgoingCallNode child1 = (OutgoingCallNode) children.get(1);
		clickNode(outgoingTree, child1);
		waitForTree(outgoingTree);
		Address expectedAddress1 = child1.getSourceAddress();
		assertEquals("Node selection did not trigger navigation", getListingAddress(),
			expectedAddress1);

		//
		// now de-select the action and make so no navigation takes place
		//
		goTo(startAddress);

		performAction(navigateAction, true);// don't track selection

		rootNode = getRootNode(outgoingTree);
		children = rootNode.getChildren();
		assertTrue(
			"Outgoing tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		child0 = (OutgoingCallNode) children.get(0);
		clickNode(outgoingTree, child0);
		waitForTree(outgoingTree);
		assertEquals("Node selection triggered navigation when the action is disabled",
			getListingAddress(), startAddress);

		child1 = (OutgoingCallNode) children.get(1);
		clickNode(outgoingTree, child1);
		waitForTree(outgoingTree);
		assertEquals("Node selection triggered navigation when the action is disabled",
			getListingAddress(), startAddress);
	}

	@Test
	public void testCallTreeForExternalFicticiousFunction() {
		// 
		// Apparently, we create fake function markup for external functions.  Thus, there is no
		// real function at that address and our plugin has to do some work to find out where
		// we 'hang' references to the external function, which is itself a Function.  These 
		// fake function will usually just be a pointer to another function.
		//

		// Setup external call linkage, 2000 -> PTR@10100 -> GDI32.DLL:LineTo
		String addrString = "10100";
		applyCmd(program, new CreateDataCmd(addr(addrString), true, PointerDataType.dataType));
		applyCmd(program,
			new CreateExternalFunctionCmd("GDI32.DLL", "LineTo", null, SourceType.IMPORTED));
		applyCmd(program, new SetExternalRefCmd(addr(addrString), 0, "GDI32.DLL", "LineTo", null,
			RefType.DATA, SourceType.IMPORTED));
		applyCmd(program, new AddMemRefCmd(addr("2000"), addr(addrString), RefType.INDIRECTION,
			SourceType.ANALYSIS, 0));
		applyCmd(program, new SetExternalRefCmd(addr("2000"), Reference.MNEMONIC, "GDI32.DLL",
			"LineTo", null, RefType.COMPUTED_CALL, SourceType.ANALYSIS));

		setProviderFunction(addrString);

		waitForTree(incomingTree);
		GTreeNode rootNode = getRootNode(incomingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue("Incoming tree does not have callers as expected for function: " + addrString,
			children.size() > 0);
	}

	@Test
	public void testRenamingIncomingFunction() {
		//
		// Test that renaming a function in the incoming tree will rename the node, if it is
		// visible.
		//
		setProviderFunction("0x5000");

		waitForTree(incomingTree);
		GTreeNode rootNode = getRootNode(incomingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Incoming tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		GTreeNode child1 = children.get(0);
		String nodeName = child1.getName();

		String originalName = "0x4000";
		Function incomingFunction = getFunction(addr(originalName));
		assertEquals(incomingFunction.getName(), nodeName);

		String newName = "bob";
		renameFunction(incomingFunction, newName);

		rootNode = getRootNode(incomingTree);
		children = rootNode.getChildren();
		assertTrue(
			"Incoming tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		assertContainsChild(children, newName);
		assertDoesntContainChild(children, originalName);
	}

	@Test
	public void testRenamingIncomingRootFunction() {
		setProviderFunction("0x5000");

		waitForTree(incomingTree);
		GTreeNode rootNode = getRootNode(incomingTree);
		List<GTreeNode> children = rootNode.getChildren();
		assertTrue(
			"Incoming tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		Function rootFunction = getFunction(addr("0x5000"));
		assertEquals("Incoming References - " + rootFunction.getName(), rootNode.getName());

		String newName = "bob";
		renameFunction(rootFunction, newName);

		rootNode = getRootNode(incomingTree);
		children = rootNode.getChildren();
		assertTrue(
			"Incoming tree does not have callers as expected for function: " + getListingFunction(),
			children.size() > 0);

		assertEquals("Incoming References - " + newName, rootNode.getName());
	}

//==================================================================================================
// Private Methods
//==================================================================================================

	private void assertProviderMatchesListingFunction() {
		assertEquals("Provider's location does not match that of the listing.",
			getListingFunction(), providerFunction());
	}

	private void toggleFollowIncomingNavigation(boolean selected) {
		ToggleDockingAction navigateIncomingLoctionsAction =
			(ToggleDockingAction) getAction("Navigation Incoming Location Changes");
		setToggleActionSelected(navigateIncomingLoctionsAction, provider.getActionContext(null),
			selected);
	}

	private void assertContainsChild(List<GTreeNode> children, String name) {
		for (GTreeNode node : children) {
			if (node.getName().equals(name)) {
				return;
			}
		}

		Assert.fail("Unable to find expected node by name: " + name);
	}

	private void assertDoesntContainChild(List<GTreeNode> children, String name) {
		for (GTreeNode node : children) {
			if (node.getName().equals(name)) {
				Assert.fail("Found unexpected node by name: " + name);
			}
		}
	}

	private GTreeNode getRootNode(GTree tree) {
		return getRootNode(tree, false);
	}

	private GTreeNode getRootNode(final GTree tree, boolean filtered) {
		waitForTree(tree);
		final AtomicReference<GTreeNode> ref = new AtomicReference<>();
		runSwing(() -> ref.set(filtered ? tree.getViewRoot() : tree.getModelRoot()));
		return ref.get();
	}

	private void renameFunction(Function function, String newName) {

		SetFunctionNameCmd cmd =
			new SetFunctionNameCmd(function.getEntryPoint(), newName, SourceType.USER_DEFINED);
		boolean result = false;

		int txID = program.startTransaction("Test - Create Function");
		try {
			result = cmd.applyTo(program);
		}
		finally {
			program.endTransaction(txID, true);
		}

		assertTrue("Failed to rename function: " + cmd.getStatusMsg(), result);
		program.flushEvents();
		waitForSwing();
	}

	private Function getFunction(Address address) {
		FunctionManager functionManager = program.getFunctionManager();
		return functionManager.getFunctionAt(address);
	}

	private void assertOutgoingNoNode(String name, int depth, boolean filtered) {
		List<NodeDepthInfo> nodes = getNodesByDepth(false, filtered);
		for (NodeDepthInfo info : nodes) {
			String nodeName = info.node.getName();
			if (nodeName.indexOf(name) != -1) {
				Assert.fail("Found node that should have been filtered out - depth: " + depth +
					"; node: " + info);
			}
		}
	}

	private void assertOutgoingNode(String name, int depth) {
		List<NodeDepthInfo> nodes = getNodesByDepth(false);
		List<NodeDepthInfo> matches = new ArrayList<>();
		for (NodeDepthInfo info : nodes) {
			String nodeName = info.node.getName();
			if (nodeName.indexOf(name) != -1) {
				matches.add(info);
			}
		}

		for (NodeDepthInfo info : matches) {
			if (info.depth == depth) {
				// found one!
				return;
			}
		}

		Assert.fail("Unable to find a node by name: " + name + " at depth: " + depth);
	}

	private void assertOutgoingMaxDepth(int depth, boolean filtered) {
		List<NodeDepthInfo> nodes = getNodesByDepth(false, filtered);
		NodeDepthInfo maxDepthNode = nodes.get(nodes.size() - 1);

		assertEquals("Node max depth does not match: " + maxDepthNode, depth, maxDepthNode.depth);
	}

	private void setOutgoingFilter(final String text) {
		runSwing(() -> provider.setOutgoingFilter(text));
		waitForTree(outgoingTree);
	}

	private void setProviderFunction(String address) {
		final Address addr = addr(address);
		goTo(addr);
		runSwing(() -> {
			ProgramLocation location = new ProgramLocation(program, addr);
			invokeInstanceMethod("doSetLocation", provider, new Class[] { ProgramLocation.class },
				new Object[] { location });
		});

		waitForSwing();
		waitForTree(incomingTree);
		waitForTree(outgoingTree);
	}

	private GTreeNode selectIncomingNode(String text) {
		GTreeNode rootNode = getRootNode(incomingTree);
		GTreeNode node = findNode(rootNode, text);
		assertNotNull(node);
		incomingTree.setSelectedNode(node);
		waitForTree(incomingTree);
		return node;
	}

	private GTreeNode selectOutgoingNode(String text) {
		GTreeNode rootNode = getRootNode(outgoingTree);
		GTreeNode node = findNode(rootNode, text);
		assertNotNull(node);
		outgoingTree.setSelectedNode(node);
		waitForTree(outgoingTree);
		return node;
	}

	private GTreeNode findNode(GTreeNode node, String text) {
		String name = node.getName();
		if (name.indexOf(text) != -1) {
			return node;
		}

		if (node instanceof GTreeSlowLoadingNode) {
			boolean loaded = ((GTreeSlowLoadingNode) node).isLoaded();
			if (!loaded) {
				return null;// children not loaded--don't load
			}
		}

		List<GTreeNode> children = node.getChildren();
		for (GTreeNode child : children) {
			GTreeNode foundNode = findNode(child, text);
			if (foundNode != null) {
				return foundNode;
			}
		}
		return null;
	}

	private void assertDepth(GTreeNode node, int depth) {
		int currentDepth = node.getTreePath().getPathCount() - 1;
		int maxNodeDepth = getMaxNodeDepth(node, currentDepth);
		assertEquals("Node depth is not correct " + node, depth, maxNodeDepth);
	}

	private int getMaxNodeDepth(GTreeNode node, int currentDepth) {
		int maxDepth = currentDepth;
		if (node instanceof GTreeSlowLoadingNode) {
			boolean loaded = ((GTreeSlowLoadingNode) node).isLoaded();
			if (!loaded) {
				return maxDepth;// children not loaded--don't load
			}
		}

		List<GTreeNode> children = node.getChildren();
		for (GTreeNode child : children) {
			maxDepth = Math.max(maxDepth, getMaxNodeDepth(child, currentDepth + 1));
		}

		return maxDepth;
	}

	private void fullyExpandIncomingNode(GTreeNode node) {
		DockingActionIf expandAction = getLocalAction(provider, "Fully Expand Selected Nodes");
		performAction(expandAction, new ActionContext(provider, incomingTree), false);
		waitForTree(node.getTree());
	}

	private void fullyExpandOutgoingNode(GTreeNode node) {
		DockingActionIf expandAction = getLocalAction(provider, "Fully Expand Selected Nodes");
		performAction(expandAction, new ActionContext(provider, outgoingTree), false);
		waitForTree(node.getTree());
	}

	private void assertIncomingNode(String name, int depth) {
		List<NodeDepthInfo> nodes = getNodesByDepth(true);
		List<NodeDepthInfo> matches = new ArrayList<>();
		for (NodeDepthInfo info : nodes) {
			String nodeName = info.node.getName();
			if (nodeName.indexOf(name) != -1) {
				matches.add(info);
			}
		}

		for (NodeDepthInfo info : matches) {
			if (info.depth == depth) {
				// found one!
				return;
			}
		}

		Assert.fail("Unable to find a node by name: " + name + " at depth: " + depth);
	}

	private void assertIncomingNoNode(String name, int depth, boolean filtered) {
		List<NodeDepthInfo> nodes = getNodesByDepth(true, filtered);
		for (NodeDepthInfo info : nodes) {
			String nodeName = info.node.getName();
			if (nodeName.indexOf(name) != -1) {
				Assert.fail("Found node that should have been filtered out - depth: " + depth +
					"; node: " + info);
			}
		}
	}

	private void assertIncomingMaxDepth(int depth, boolean filtered) {
		List<NodeDepthInfo> nodes = getNodesByDepth(true, filtered);
		NodeDepthInfo maxDepthNode = nodes.get(nodes.size() - 1);

		assertEquals("Node max depth does not match: " + maxDepthNode, depth, maxDepthNode.depth);
	}

	private List<NodeDepthInfo> getNodesByDepth(boolean incoming) {
		return getNodesByDepth(incoming, false);
	}

	private List<NodeDepthInfo> getNodesByDepth(boolean incoming, boolean filtered) {
		List<NodeDepthInfo> list = new ArrayList<>();
		GTreeNode root =
			incoming ? getRootNode(incomingTree, filtered) : getRootNode(outgoingTree, filtered);
		accumulateNodeDepths(list, root, 0);
		Collections.sort(list);
		return list;
	}

	private void accumulateNodeDepths(List<NodeDepthInfo> list, GTreeNode node, int depth) {

		NodeDepthInfo info = new NodeDepthInfo(node, depth);
		if (!list.contains(info)) {
			list.add(info);
		}

		if (node instanceof GTreeSlowLoadingNode) {
			boolean loaded = ((GTreeSlowLoadingNode) node).isLoaded();
			if (!loaded) {
				return;// children not loaded--don't load
			}
		}

		List<GTreeNode> children = node.getChildren();
		for (GTreeNode child : children) {
			accumulateNodeDepths(list, child, depth + 1);
		}
	}

	private void setIncomingFilter(final String text) {
		runSwing(() -> provider.setIncomingFilter(text));
		waitForTree(incomingTree);
	}

	private void setDepth(final int depth) {
		runSwing(() -> provider.setRecurseDepth(depth));
	}

	private int currentDepthSetting(CallTreeProvider aProvider) {
		return aProvider.getRecurseDepth();
	}

	private Function getListingFunction() {
		FunctionManager functionManager = program.getFunctionManager();
		ProgramLocation codeBrowserLocation = codeBrowserPlugin.getCurrentLocation();
		return functionManager.getFunctionContaining(codeBrowserLocation.getAddress());
	}

	private Function providerFunction() {
		return (Function) getInstanceField("currentFunction", provider);
	}

	private ProgramLocation currentLocation() {
		return codeBrowserPlugin.getCurrentLocation();
	}

	private Address getListingAddress() {
		return codeBrowserPlugin.getCurrentAddress();
	}

	private Map<String, Integer> createNameCountMap(GTreeNode node) {
		Map<String, Integer> map = new HashMap<>();
		List<GTreeNode> children = node.getChildren();
		for (GTreeNode child : children) {
			String name = child.getName();
			Integer integer = map.get(name);
			if (integer == null) {
				integer = 0;
			}
			int asInt = integer;
			asInt++;
			map.put(name, asInt);
		}
		return map;
	}

	private void assertDuplicateChildStatus(Map<String, Integer> childCountMap,
			boolean shouldHaveDuplicates) {
		boolean foundDuplicates = false;
		Set<Entry<String, Integer>> entrySet = childCountMap.entrySet();
		for (Entry<String, Integer> entry : entrySet) {
			int value = entry.getValue();
			if (value != 1) {
				foundDuplicates = true;
			}
		}

		String errorMessage =
			(shouldHaveDuplicates ? "Did not find " : "Found") + " duplicate child entries";
		assertEquals(errorMessage, shouldHaveDuplicates, foundDuplicates);
	}

	private void clickNode(final GTree tree, GTreeNode node) {
		Rectangle pathBounds = tree.getPathBounds(node.getTreePath());
		JTree jTree = (JTree) AbstractGenericTest.getInstanceField("tree", tree);
		int x = pathBounds.x + 2;
		int y = pathBounds.y + 2;
		clickMouse(jTree, MouseEvent.BUTTON1, x, y, 1, 0);
	}

	private DockingActionIf getAction(String actionName) {
		DockingActionIf action = getLocalAction(provider, actionName);
		return action;
	}

	/**
	 * Shows and returns a provider for the specified address.
	 */
	private CallTreeProvider showProvider(String address) {
		goTo(addr(address));
		ProgramLocation location = codeBrowserPlugin.getCurrentLocation();

		DockingAction action = callTreePlugin.getShowCallTreeFromMenuAction();
		performAction(action);

		return runSwing(() -> callTreePlugin.findTransientProviderForLocation(location));
	}

	private Address addr(String address) {
		return builder.addr(address);
	}

	private void goTo(final Address address) {
		runSwing(() -> goToService.goTo(address));
	}

//==================================================================================================
// Inner Classes
//==================================================================================================

	private class NodeDepthInfo implements Comparable<NodeDepthInfo> {
		private GTreeNode node;
		private int depth;

		NodeDepthInfo(GTreeNode node, int depth) {
			this.node = node;
			this.depth = depth;
		}

		@Override
		public int compareTo(NodeDepthInfo o) {
			if (depth != o.depth) {
				return depth - o.depth;
			}

			String myName = node.getName();
			String otherName = o.node.getName();
			if (!myName.equals(otherName)) {
				return myName.compareTo(otherName);
			}

			// something suitable
			return System.identityHashCode(node) - System.identityHashCode(o.node);
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result + depth;
			result = prime * result + ((node == null) ? 0 : node.hashCode());
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj) {
				return true;
			}
			if (obj == null) {
				return false;
			}
			if (getClass() != obj.getClass()) {
				return false;
			}
			NodeDepthInfo other = (NodeDepthInfo) obj;
			if (!getOuterType().equals(other.getOuterType())) {
				return false;
			}
			if (depth != other.depth) {
				return false;
			}
			if (node == null) {
				if (other.node != null) {
					return false;
				}
			}
			else if (!node.equals(other.node)) {
				return false;
			}
			return true;
		}

		private CallTreePluginTest getOuterType() {
			return CallTreePluginTest.this;
		}

		@Override
		public String toString() {
			return "depth=" + depth + ", path: " + getPath();
		}

		private String getPath() {
			StringBuilder buffy = new StringBuilder();
			TreePath treePath = node.getTreePath();
			Object[] path = treePath.getPath();
			for (Object object : path) {
				if (buffy.length() != 0) {
					buffy.append(" -> ");
				}
				buffy.append(object);
			}
			return buffy.toString();
		}
	}
}