/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.openide.windows;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.DefaultKeyboardFocusManager;
import java.awt.EventQueue;
import java.awt.GraphicsEnvironment;
import java.awt.KeyboardFocusManager;
import java.awt.event.ActionEvent;
import java.beans.FeatureDescriptor;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JButton;
import javax.swing.JTextField;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import junit.framework.Test;
import junit.framework.TestSuite;
import org.netbeans.junit.NbTestCase;
import org.openide.awt.Actions;
import org.openide.cookies.CloseCookie;
import org.openide.cookies.EditCookie;
import org.openide.cookies.OpenCookie;
import org.openide.cookies.SaveCookie;
import org.openide.cookies.ViewCookie;
import org.openide.nodes.AbstractNode;
import org.openide.nodes.Children;
import org.openide.nodes.FilterNode;
import org.openide.nodes.Node;
import org.openide.util.ContextAwareAction;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.Lookup.Result;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.Utilities;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;

/**
 * Check the behaviour of TopComponent's lookup.
 * @author Jaroslav Tulach, Jesse Glick
 */
public class TopComponentGetLookupTest extends NbTestCase {

    public static Test suite() {
        return GraphicsEnvironment.isHeadless() ? new TestSuite() : new TestSuite(TopComponentGetLookupTest.class);
    }

    static {
        System.setProperty("org.openide.windows.DummyWindowManager.VISIBLE", "false");
    }
    
    /** top component we call set on*/
    protected TopComponent top;
    /** top component we call get on */
    protected TopComponent get;
    /** its lookup */
    protected Lookup lookup;

    private Logger LOG = Logger.getLogger("TEST-" + getName());
    
    public TopComponentGetLookupTest(String testName) {
        super(testName);
    }
    
    /** Setup component with lookup.
     */
    @Override
    protected void setUp() {
        top = new TopComponent();
        get = top;
        lookup = top.getLookup();
        defaultFocusManager.setC(null);
    }
    
    @Override
    protected boolean runInEQ() {
        return true;
    }
    
    @Override
    protected Level logLevel() {
        return Level.FINER;
    }
    
    /** Test to find nodes.
     */
    private <T> void doTestNodes(Node[] arr, Class<T> c, int cnt) {
        LOG.fine("setActivatedNodes: " + arr);
        if (arr != null) {
            top.setActivatedNodes(arr);
        }
        
        assertNotNull("At least one node is registered", lookup.lookup(c));
        LOG.fine("before lookup");
        Lookup.Result<T> res = lookup.lookup(new Lookup.Template<T>(c));
        Collection<? extends Lookup.Item<T>> coll = res.allItems();
        LOG.fine("after lookup");
        assertEquals("Two registered: " + coll, cnt, coll.size());
    }

    public void testNodes() {
        doTestNodes(new Node[] {new N("1"), new N("2")}, N.class, 2);
        doTestNodes(new Node[] {new N("1"), new N("2")}, FeatureDescriptor.class, 2);
    }
    
    private <T> void doTestNodesWithChangesInLookup(Class<T> c) {
        InstanceContent ic = new InstanceContent();
        
        LOG.fine("do test nodes");
        Node[] arr = new Node[] {
            new AbstractNode(Children.LEAF, new AbstractLookup(ic)),
            new AbstractNode(Children.LEAF, Lookup.EMPTY),
        };
        arr[0].setName("cookie-container-node");
        arr[1].setName("node-as-cookie");
        //doTestNodes(arr, AbstractNode.class);
        doTestNodes(arr, c, 2);
        
        LOG.fine("add new node");
        ic.add(arr[1]);
        
        /* Huh? There should be both [0] and [1], how can you say which one will be returned?
        assertEquals ("Now the [1] is in lookup of [0]", arr[1], lookup.lookup (c));
         */
        Collection<? extends T> all = lookup.lookup(new Lookup.Template<T>(c)).allInstances();
        LOG.fine("query");
        assertEquals("Two nodes are in TC lookup", 2, all.size());
        assertEquals("They are the ones we expect", new HashSet<Node>(Arrays.asList(arr)), new HashSet<T>(all));
        assertTrue("Lookup simple query gives one or the other", new HashSet<Node>(Arrays.asList(arr)).contains(lookup.lookup(c)));
        assertEquals("Have two lookup items", 2, lookup.lookup(new Lookup.Template<T>(c)).allItems().size());
        
        LOG.fine("last doTestNodes");
        doTestNodes(null, c, 2);
        LOG.fine("after last doTestNodes");
    }
    
    public void testNodesWhenTheyAreNotInTheirLookup() {
        doTestNodesWithChangesInLookup(AbstractNode.class);
    }
    
    public void testNodesSuperclassesWhenTheyAreNotInTheirLookup() {
        doTestNodesWithChangesInLookup(FeatureDescriptor.class);
    }
    
    public void testFilterNodeProblems() {
        class CookieN extends AbstractNode implements Node.Cookie {
            public CookieN() {
                super(Children.LEAF);
                getCookieSet().add(this);
            }
            
        }
        
        CookieN n = new CookieN();
        FilterNode fn = new FilterNode(n);
        top.setActivatedNodes(new Node[] { fn });
        assertTrue("CookieN is in FilterNode lookup", n == fn.getLookup().lookup(CookieN.class));
        assertTrue("CookieN is in TopComponent", n == lookup.lookup(CookieN.class));
        assertEquals("Just one node", 1, lookup.lookup(new Lookup.Template<Node>(Node.class)).allItems().size());
        assertTrue("Plain cookie found", n == lookup.lookup(Node.Cookie.class));
    }
    
    
    /** Tests changes in cookies.
     */
    public void testCookies() {
        N[] arr = { new N("1"), new N("2"), new N("3") };

        LOG.fine("before activating");
        top.setActivatedNodes(arr);
        LOG.fine("After activating");
        Node[] myArr = get.getActivatedNodes();
        LOG.fine("Nodes got back: " + myArr + " from " + top);
        if (myArr != null) {
            LOG.fine("  here they are: " + Arrays.asList(myArr));
        }
        assertEquals("Three nodes there", 3, myArr.length);
        LOG.fine("Correctly activated");
        
        L l = new L();
        Lookup.Result<OpenCookie> res = lookup.lookup(new Lookup.Template<OpenCookie>(OpenCookie.class));
        res.addLookupListener(l);
        LOG.fine("Listener attached");
        
        assertEquals("Empty now", res.allItems().size(), 0);

        LOG.fine("Changing state to 0x01");
        arr[0].state(0x01); // check open cookie
        LOG.fine("Changing state to 0x01 done");
        
        assertEquals("One item", res.allItems().size(), 1);
        l.check("One change", 1);

        LOG.fine("Changing state to 0x02");
        arr[2].state(0x02); // change of different cookie
        LOG.fine("Changing state to 0x02 done");
        
        assertEquals("Still one item", res.allItems().size(), 1);
        l.check("No change", 0);
        
        LOG.fine("Changing state to 0x03");
        arr[2].state(0x03); // added also OpenCookie
        LOG.fine("Changing state to 0x03 done");
        
        assertEquals("Both items", res.allItems().size(), 2);
        l.check("One change again", 1);
        
        LOG.fine("Changing state to 0x00");
        arr[0].state(0x00);
        LOG.fine("Changing state to 0x00 done");
        
        assertEquals("One still there", res.allItems().size(), 1);
        assertEquals("The second object", lookup.lookup(OpenCookie.class), arr[2].getCookie(OpenCookie.class));
        
        LOG.fine("Clearing activated nodes");
        top.setActivatedNodes(new Node[0]);
        LOG.fine("Clear done");
        assertNull("No cookie now", lookup.lookup(OpenCookie.class));
    }
    
    public void testNodesAreInTheLookupAndNothingIsFiredBeforeFirstQuery() {
        AbstractNode n1 = new AbstractNode(Children.LEAF, Lookup.EMPTY);
        top.setActivatedNodes(new Node[] { n1 });
        assertEquals("One node there", 1, get.getActivatedNodes().length);
        assertEquals("Is the right now", n1, get.getActivatedNodes()[0]);
        
        Lookup.Result<Node> res = lookup.lookup(new Lookup.Template<Node>(Node.class));
        L l = new L();
        res.addLookupListener(l);
        
        l.check("Nothing fired before first query", 0);
        res.allInstances();
        l.check("Nothing is fired on first query", 0);
        lookup.lookup(new Lookup.Template<Node>(Node.class)).allInstances();
        l.check("And additional query does not change anything either", 0);
    }
    
    public void testNodesAreThereEvenIfTheyAreNotContainedInTheirOwnLookup() {
        Lookup.Result<Node> res = lookup.lookup(new Lookup.Template<Node>(Node.class));
        
        AbstractNode n1 = new AbstractNode(Children.LEAF, Lookup.EMPTY);
        
        InstanceContent content = new InstanceContent();
        AbstractNode n2 = new AbstractNode(Children.LEAF, new AbstractLookup(content));
        
        assertNull("Not present in its lookup", n1.getLookup().lookup(n1.getClass()));
        assertNull("Not present in its lookup", n2.getLookup().lookup(n2.getClass()));
        
        top.setActivatedNodes(new AbstractNode[] { n1 });
        assertEquals("But node is in the lookup", n1, lookup.lookup(n1.getClass()));
        
        assertEquals("One item there", 1, res.allInstances().size());
        
        L listener = new L();
        res.addLookupListener(listener);
        
        top.setActivatedNodes(new AbstractNode[] { n2 });
        assertEquals("One node there", 1, get.getActivatedNodes().length);
        assertEquals("n2", n2, get.getActivatedNodes()[0]);
        
        //MK - here it changes twice.. because the setAtivatedNodes is trigger on inner TC, then lookup of MVTC contains old activated node..
        // at this monent the merged lookup contains both items.. later it gets synchronized by setting the activated nodes on the MVTC as well..
        // then it contains only the one correct node..
        listener.check("Node changed", 1);
        
        Collection addedByTCLookup = res.allInstances();
        assertEquals("One item still", 1, addedByTCLookup.size());
        
        content.add(n2);
        assertEquals("After the n2.getLookup starts to return itself, there is no change",
                addedByTCLookup, res.allInstances());
        
        // this could be commented out if necessary:
        listener.check("And nothing is fired", 0);
        
        content.remove(n2);
        assertEquals("After the n2.getLookup stops to return itself, there is no change",
                addedByTCLookup, res.allInstances());
        // this could be commented out if necessary:
        listener.check("And nothing is fired", 0);
        
        content.add(n1);
        // this could be commented out if necessary:
        listener.check("And nothing is fired", 0);
        // Change from former behavior (#36336): we don't *want* n1 in res.
        Collection one = res.allInstances();
        assertEquals("Really just the activated node", 1, one.size());
        Iterator it = one.iterator();
        assertEquals("It is the one added by the TC lookup", n2, it.next());
    }
    
    public void testNoChangeWhenSomethingIsChangedOnNotActivatedNode() {
        doTestNoChangeWhenSomethingIsChangedOnNotActivatedNode(0);
    }
    
    public void testNoChangeWhenSomethingIsChangedOnNotActivatedNode2() {
        doTestNoChangeWhenSomethingIsChangedOnNotActivatedNode(50);
    }
    
    private void doTestNoChangeWhenSomethingIsChangedOnNotActivatedNode(int initialSize) {
        Object obj = new OpenCookie() { public void open() {} };
        
        Lookup.Result<OpenCookie> res = lookup.lookup(new Lookup.Template<OpenCookie>(OpenCookie.class));
        Lookup.Result<Node> nodeRes = lookup.lookup(new Lookup.Template<Node>(Node.class));
        
        InstanceContent ic = new InstanceContent();
        CountingLookup cnt = new CountingLookup(ic);
        AbstractNode ac = new AbstractNode(Children.LEAF, cnt);
        for (int i = 0; i < initialSize; i++) {
            ic.add(new Integer(i));
        }
        
        top.setActivatedNodes(new Node[] { ac });
        assertEquals("One node there", 1, get.getActivatedNodes().length);
        assertEquals("It is the ac one", ac, get.getActivatedNodes()[0]);
        ic.add(obj);
        
        L listener = new L();
        
        res.allItems();
        nodeRes.allItems();
        res.addLookupListener(listener);
        
        Collection allListeners = cnt.listeners;
        
        assertEquals("Has the cookie", 1, res.allItems().size());
        listener.check("No changes yet", 0);
        
        ic.remove(obj);
        
        assertEquals("Does not have the cookie", 0, res.allItems().size());
        listener.check("One change", 1);
        
        top.setActivatedNodes(new N[0]);
        assertEquals("The nodes are empty", 0, get.getActivatedNodes().length);
        listener.check("No change", 0);
        
        cnt.queries = 0;
        ic.add(obj);
        ic.add(ac);
        listener.check("Removing the object or node from not active node does not send any event", 0);
        
        nodeRes.allItems();
        listener.check("Queriing for node does generate an event", 0);
        assertEquals("No Queries to the not active node made", 0, cnt.queries);
        assertEquals("No listeneners on cookies", allListeners, cnt.listeners);
    }
    
    public void testBug32470FilterNodeAndANodeImplementingACookie() {
        class NY extends AbstractNode implements SaveCookie {
            public NY() {
                super(Children.LEAF);
                getCookieSet().add(this);
            }
            
            public void save() {
            }
        }
        
        Node ny = new NY();
        Node node = new FilterNode(new FilterNode(ny, null, ny.getLookup()));
        top.setActivatedNodes(new Node[] { node });
        
        Lookup.Template<Node> nodeTemplate = new Lookup.Template<Node>(Node.class);
        Lookup.Template<SaveCookie> saveTemplate = new Lookup.Template<SaveCookie>(SaveCookie.class);
        Collection<? extends Node> res;
        
        res = lookup.lookup(nodeTemplate).allInstances();
        
        assertEquals("just one returned", res.size(), 1);
        assertEquals("node is node", node, res.iterator().next());
        //MK - the above 2 tests should test the same..
        //        assertEquals ("FilterNode is the only node there",
        //            Collections.singletonList(node), res
        //        );
        
        Collection<? extends SaveCookie> res2 = lookup.lookup(saveTemplate).allInstances();
        
        assertEquals("just one returned", res2.size(), 1);
        assertEquals("node is node", ny, res2.iterator().next());
        //MK - the above 2 tests should test the same..
        //        assertEquals ("SaveCookie is there only once",
        //            Collections.singletonList(ny), res
        //        );
        
        res = lookup.lookup(nodeTemplate).allInstances();
        
        assertEquals("just one returned", res.size(), 1);
        assertEquals("node is node", node, res.iterator().next());
        //MK - the above 2 tests should test the same..
        //        assertEquals ("FilterNode is still the only node there",
        //            Collections.singletonList(node), res
        //        );
    }
    
    public void testActionMapIsTakenFromComponentAndAlsoFromFocusedOne() {
        JTextField panel = new JTextField();
        defaultFocusManager.setC(panel);
        
        top.add(BorderLayout.CENTER, panel);

        class Act extends AbstractAction {
            public void actionPerformed(ActionEvent ev) {
            }
        }
        Act act1 = new Act();
        Act act2 = new Act();
        Act act3 = new Act();

        top.getActionMap().put("globalRegistration", act1);
        top.getActionMap().put("doubleRegistration", act2);

        panel.getActionMap().put("doubleRegistration", act3);
        panel.getActionMap().put("focusedRegistration", act3);

        final ActionMap map = (ActionMap)top.getLookup().lookup(ActionMap.class);

        assertEquals("actions registered directly on TC are found", act1, map.get("globalRegistration"));
        assertEquals("even if they are provided by focused component", act2, map.get("doubleRegistration"));

        assertEquals("actions are delegated to focus owner, if not present", act3, map.get("focusedRegistration"));

        List<Object> keys = Arrays.asList(map.allKeys());
        assertTrue("focusedRegistration is there: " + keys, keys.contains("focusedRegistration"));
        assertTrue("doubleRegistration is there: " + keys, keys.contains("doubleRegistration"));
        assertTrue("globalRegistration is there: " + keys, keys.contains("globalRegistration"));
        assertTrue("closeWindow is by default there: " + keys, keys.contains("closeWindow"));
        
        top.open();
        top.requestActive();
        
        final Result<ActionMap> res = Utilities.actionsGlobalContext().lookupResult(ActionMap.class);
        class Checker implements LookupListener {
            List<Object> keys1, keys2;
            @Override
            public void resultChanged(LookupEvent ev) {
                Object[] arr = map.allKeys(); 
                if (arr == null) {
                    arr = new Object[0];
                }
                
                if (keys1 == null) {
                    keys1 = Arrays.asList(arr);
                } else {
                    assertNull("At most two calls", keys2);
                    keys2 = Arrays.asList(arr);
                }
            }
        }
        Checker listener = new Checker();
        res.addLookupListener(listener);
        assertEquals("One map", 1, res.allInstances().size());
        
        JTextField f = new JTextField();
        f.getActionMap().put("focusedRegistration", act3);
        defaultFocusManager.setC(f);
        assertEquals("but as it is not in the right component, nothing is found", null, map.get("focusedRegistration"));
        res.removeLookupListener(listener);
        
        assertNotNull("Two changes delivered", listener.keys2);
        assertTrue("doubleRegistration was there: " + listener.keys1, listener.keys1.contains("doubleRegistration"));
        assertTrue("globalRegistration was there: " + listener.keys1, listener.keys1.contains("globalRegistration"));
        assertTrue("closeWindow was there: " + listener.keys1, listener.keys1.contains("closeWindow"));
        assertTrue("focusedRegistration was there: " + listener.keys1, listener.keys1.contains("focusedRegistration"));
        
        assertFalse("focusedRegistration was there: " + listener.keys2, listener.keys2.contains("focusedRegistration"));
        assertTrue("doubleRegistration was there: " + listener.keys2, listener.keys2.contains("doubleRegistration"));
        assertTrue("globalRegistration was there: " + listener.keys2, listener.keys2.contains("globalRegistration"));
        assertTrue("closeWindow was there: " + listener.keys2, listener.keys2.contains("closeWindow"));
    }
    
    public void testActionMapFromFocusedOneButNotOwnButton() {
        ContextAwareAction a = Actions.callback("find", null, false, "Find", null, false);
        
        class Act extends AbstractAction {
            int cnt;
            Action check;
            
            @Override
            public void actionPerformed(ActionEvent ev) {
                cnt++;
            }
        }
        Act act1 = new Act();
        Act act3 = new Act();

        Action action = a;
        act1.check = action;
        act3.check = action;
        final JButton disabled = new JButton();
        top.add(BorderLayout.CENTER, disabled);
        final JButton f = new JButton(action);
        class L implements PropertyChangeListener {
            private int cnt;
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                assertEquals("enabled", evt.getPropertyName());
                cnt++;
            }
        }
        L listener = new L();
        action.addPropertyChangeListener(listener);
        top.add(BorderLayout.SOUTH, f);
        defaultFocusManager.setC(top);

        disabled.getActionMap().put("find", act3);
        top.open();
        top.requestActive();
        assertFalse("Disabled by default", action.isEnabled());
        assertFalse("Button disabled too", f.isEnabled());
        assertEquals("no change yet", 0, listener.cnt);
        
        defaultFocusManager.setC(disabled);
        
        assertEquals("One change", 1, listener.cnt);
        assertTrue("Still enabled", action.isEnabled());
        assertTrue("Button enabled too", f.isEnabled());
        
        f.getModel().addChangeListener(new ChangeListener() {

            @Override
            public void stateChanged(ChangeEvent e) {
                if (f.getModel().isPressed()) {
                    defaultFocusManager.setC(f);
                } else {
                    defaultFocusManager.setC(disabled);
                }
            }
        });
        f.doClick();

        assertEquals("Not Delegated to act1", 0, act1.cnt);
        assertEquals("Delegated to act3", 1, act3.cnt);
    }
    
    
    public void testChangingNodesDoesNotChangeActionMap() {
        N node = new N("testChangingNodesDoesNotChangeActionMap");
        node.state(0x00);
        top.setActivatedNodes(new Node[] { node });
        
        Lookup.Result<ActionMap> res = lookup.lookup(new Lookup.Template<ActionMap>(ActionMap.class));
        assertEquals("One item there", 1, res.allInstances().size());
        ActionMap map = (ActionMap)res.allInstances().toArray()[0];
        
        L l = new L();
        res.addLookupListener(l);
        
        node.state(0x01);
        
        assertEquals("Map is still the same", map, res.allInstances().toArray()[0]);
        
        l.check("No change in lookup", 0);
        
        top.setActivatedNodes(new Node[] { Node.EMPTY });
        assertEquals("Map remains the same", map, res.allInstances().toArray()[0]);
        
        l.check("There is no change", 0);
        
    }
    
    public void testMapKeys45323() {
        assertNotNull(top.getActionMap().keys());
    }
    
    
    /**
     * Check that even if a node has a <em>different</em> node in its lookup, a
     * query on Node.class will produce only the actual activated nodes.
     * Other queries may return the embedded node, but not duplicates.
     * @see "#36336"
     */
    public void testForeignNodesInLookupIgnoredForNodeQuery() throws Exception {
        class CloseCookieNode extends AbstractNode implements CloseCookie {
            CloseCookieNode() {
                super(Children.LEAF);
                setName("n1");
            }
            public boolean close() {return true;}
        }
        Node n1 = new CloseCookieNode();
        Node n2 = new AbstractNode(Children.LEAF) {
            {
                setName("n2");
                class ViewCookieNode extends AbstractNode implements ViewCookie {
                    ViewCookieNode() {
                        super(Children.LEAF);
                        setName("n3");
                    }
                    public void view() {}
                }
                getCookieSet().add(new ViewCookieNode());
                getCookieSet().add(new OpenCookie() {
                    public void open() {}
                });
            }
        };
        Node[] sel = new Node[] {n1, n2};
        assertEquals("First node in selection has CloseCookie",
                1,
                n1.getLookup().lookup(new Lookup.Template<CloseCookie>(CloseCookie.class)).allInstances().size());
        assertEquals("Second node in selection has OpenCookie",
                1,
                n2.getLookup().lookup(new Lookup.Template<OpenCookie>(OpenCookie.class)).allInstances().size());
        assertEquals("Second node in selection has ViewCookie (actually a Node)",
                1,
                n2.getLookup().lookup(new Lookup.Template<ViewCookie>(ViewCookie.class)).allInstances().size());
        ViewCookie v = (ViewCookie)n2.getCookie(ViewCookie.class);
        assertNotNull(v);
        assertTrue(v instanceof Node);
        
        HashSet<Node> queryJustOnce = new HashSet<Node>(n2.getLookup().lookup(new Lookup.Template<Node>(Node.class)).allInstances());
        assertEquals("Second node in selection has two nodes in its own lookup",
                new HashSet<Object>(Arrays.asList(new Object[] {n2, v})), queryJustOnce
                );
        assertEquals(2, queryJustOnce.size());
        top.setActivatedNodes(sel);
        assertEquals("CloseCookie propagated from one member of node selection to TC lookup",
                1,
                lookup.lookup(new Lookup.Template<CloseCookie>(CloseCookie.class)).allInstances().size());
        assertEquals("OpenCookie propagated from one member of node selection to TC lookup",
                1,
                lookup.lookup(new Lookup.Template<OpenCookie>(OpenCookie.class)).allInstances().size());
        assertEquals("ViewCookie propagated from one member of node selection to TC lookup",
                1,
                lookup.lookup(new Lookup.Template<ViewCookie>(ViewCookie.class)).allInstances().size());
        assertEquals("But TC lookup query on Node gives only selection, not cookie node",
                new HashSet<Node>(Arrays.asList(sel)),
                new HashSet<Node>(lookup.lookup(new Lookup.Template<Node>(Node.class)).allInstances()));
        assertEquals(2, lookup.lookup(new Lookup.Template<Node>(Node.class)).allInstances().size());
        assertEquals("TC lookup query on FeatureDescriptor gives all three however",
                3,
                lookup.lookup(new Lookup.Template<FeatureDescriptor>(FeatureDescriptor.class)).allInstances().size());
        top.setActivatedNodes(new Node[] {n1});
        assertEquals("After setting node selection to one node, TC lookup has only that node",
                Collections.singleton(n1),
                new HashSet<Node>(lookup.lookup(new Lookup.Template<Node>(Node.class)).allInstances()));
        assertEquals(1, lookup.lookup(new Lookup.Template<Node>(Node.class)).allInstances().size());
        assertEquals("And the OpenCookie is gone",
                0,
                lookup.lookup(new Lookup.Template<OpenCookie>(OpenCookie.class)).allInstances().size());
        assertEquals("And the ViewCookie is gone",
                0,
                lookup.lookup(new Lookup.Template<ViewCookie>(ViewCookie.class)).allInstances().size());
        assertEquals("But the CloseCookie remains",
                1,
                lookup.lookup(new Lookup.Template<CloseCookie>(CloseCookie.class)).allInstances().size());
    }
    
    public void testAssociateLookupCanBecalledJustOnce() throws Exception {
        class TC extends TopComponent {
            public TC() {
            }
            
            public TC(Lookup l) {
                super(l);
            }
            
            public void asso(Lookup l) {
                associateLookup(l);
            }
        }
        
        TC tc = new TC();
        assertNotNull("There is default lookup", tc.getLookup());
        try {
            tc.asso(Lookup.EMPTY);
            fail("Should throw an exception");
        } catch (IllegalStateException ex) {
            // ok, should be thrown
        }
        
        tc = new TC(Lookup.EMPTY);
        assertEquals("Should return the provided lookup", Lookup.EMPTY, tc.getLookup());
        
        try {
            tc.asso(Lookup.EMPTY);
            fail("Should throw an exception - second association not possible");
        } catch (IllegalStateException ex) {
            // ok, should be thrown
        }
        
        tc = new TC();
        tc.asso(Lookup.EMPTY);
        assertEquals("First association was successful", Lookup.EMPTY, tc.getLookup());
        
        try {
            tc.asso(new TC().getLookup());
            fail("Should throw an exception - second association not possible");
        } catch (IllegalStateException ex) {
            // ok, should be thrown
        }
    }
    
    /** Listener to count number of changes.
     */
    private static final class L extends Object
            implements LookupListener {
        private int cnt;
        
        /** A change in lookup occured.
         * @param ev event describing the change
         */
        public void resultChanged(LookupEvent ev) {
            cnt++;
        }
        
        /** Checks at least given number of changes.
         */
        public void checkAtLeast(String text, int num) {
            if (cnt < num) {
                fail(text + " expected at least " + num + " but was " + cnt);
            }
            cnt = 0;
        }
        
        /** Checks number of modifications.
         */
        public void check(String text, int num) {
            assertEquals(text, num, cnt);
            cnt = 0;
        }
    }
    
    
    /** Overides some methods so it is not necessary to use the data object.
     */
    protected static final class N extends AbstractNode {
        private Node.Cookie[] cookies = {
            new OpenCookie() { public void open() {} },
            new EditCookie() { public void edit() {} },
            new SaveCookie() { public void save() {} },
            new CloseCookie() { public boolean close() { return true; } },
        };
        
        private int s;
        
        public N(String name) {
            super(Children.LEAF);
            setName(name);
        }
        
        public void state(int s) {
            this.s = s;
            fireCookieChange();
        }
        
        public <T extends Node.Cookie> T getCookie(Class<T> c) {
            int mask = 0x01;
            
            for (int i = 0; i < cookies.length; i++) {
                if ((s & mask) != 0 && c.isInstance(cookies[i])) {
                    return c.cast( cookies[i] );
                }
                mask = mask << 1;
                
            }
            return null;
        }

        public String toString() {
            return "N[" + getName() + ", " + s + "]";
        }
    }
    
    private static final class CountingLookup extends Lookup {
        private Lookup delegate;
        public List<LookupListener> listeners = new ArrayList<LookupListener>();
        public int queries;
        
        public CountingLookup(InstanceContent ic) {
            delegate = new AbstractLookup(ic);
            
        }
        
        public <T> T lookup(Class<T> clazz) {
            return delegate.lookup(clazz);
        }
        
        public <T> Lookup.Result<T> lookup(Lookup.Template<T> template) {
            if (
                    !Node.Cookie.class.isAssignableFrom(template.getType()) &&
                    !Node.class.isAssignableFrom(template.getType())
                    ) {
                return delegate.lookup(template);
            }
            
            
            final Lookup.Result<T> d = delegate.lookup(template);
            
            class Wrap extends Lookup.Result<T> {
                public void addLookupListener(LookupListener l) {
                    listeners.add(l);
                    d.addLookupListener(l);
                }
                
                public void removeLookupListener(LookupListener l) {
                    listeners.remove(l);
                    d.removeLookupListener(l);
                }
                public Collection<? extends T> allInstances() {
                    queries++;
                    return d.allInstances();
                }
                public Collection<? extends Lookup.Item<T>> allItems() {
                    queries++;
                    return d.allItems();
                }
                public Set<Class<? extends T>> allClasses() {
                    queries++;
                    return d.allClasses();
                }
            }
            
            return new Wrap();
        }
        
    }
    
    static class Def extends DefaultKeyboardFocusManager {
        private Component c;
        private int cnt;

        public Def() {
            KeyboardFocusManager.setCurrentKeyboardFocusManager(this);
        }

        public void setC(Component c) {
            Object oldC = this.c;
            this.c = c;
            firePropertyChange("permanentFocusOwner", oldC, c);
        }

        @Override
        public Component getFocusOwner() {
            return null;
        }
        @Override
        public Component getPermanentFocusOwner() {
            cnt++;
            return c;
        }

    }
    private final static Def defaultFocusManager = new Def();
    static {
        try {
            Utilities.actionsGlobalContext();
            EventQueue.invokeAndWait(new Runnable() {
                @Override public void run() {}
            });
            assertEquals("actionsGlobalContext calls once into our focus manager", 1, defaultFocusManager.cnt);
        } catch (InterruptedException | InvocationTargetException ex) {
            throw new IllegalStateException(ex);
        }
    }
}
