/**
 * Copyright (c) 2016-2020 by the respective copyright holders.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package com.zsmartsystems.zigbee.app.discovery;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Future;

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatchers;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import com.zsmartsystems.zigbee.CommandResult;
import com.zsmartsystems.zigbee.IeeeAddress;
import com.zsmartsystems.zigbee.TestUtilities;
import com.zsmartsystems.zigbee.ZigBeeCommand;
import com.zsmartsystems.zigbee.ZigBeeEndpointAddress;
import com.zsmartsystems.zigbee.ZigBeeNetworkManager;
import com.zsmartsystems.zigbee.ZigBeeNode;
import com.zsmartsystems.zigbee.ZigBeeNodeStatus;
import com.zsmartsystems.zigbee.transaction.ZigBeeTransactionFuture;
import com.zsmartsystems.zigbee.transaction.ZigBeeTransactionMatcher;
import com.zsmartsystems.zigbee.zcl.ZclCommand;
import com.zsmartsystems.zigbee.zdo.ZdoCommandType;
import com.zsmartsystems.zigbee.zdo.ZdoStatus;
import com.zsmartsystems.zigbee.zdo.command.DeviceAnnounce;
import com.zsmartsystems.zigbee.zdo.command.IeeeAddressResponse;
import com.zsmartsystems.zigbee.zdo.command.NetworkAddressResponse;

/**
 *
 * @author Chris Jackson
 *
 */
public class ZigBeeNetworkDiscovererTest {
    static final int TIMEOUT = 5000;

    private ZigBeeNetworkManager networkManager;
    private ArgumentCaptor<ZigBeeNode> nodeCapture;
    private Map<Integer, ZigBeeCommand> responses = new HashMap<>();

    @Before
    public void setupTest() {
        networkManager = Mockito.mock(ZigBeeNetworkManager.class);
        nodeCapture = ArgumentCaptor.forClass(ZigBeeNode.class);

        Mockito.doAnswer(new Answer<Future<CommandResult>>() {
            @Override
            public Future<CommandResult> answer(InvocationOnMock invocation) {
                ZigBeeCommand command = (ZigBeeCommand) invocation.getArguments()[0];

                ZigBeeTransactionFuture commandFuture = new ZigBeeTransactionFuture();
                CommandResult result = new CommandResult(responses.get(command.getClusterId()));
                commandFuture.set(result);
                return commandFuture;
            }
        }).when(networkManager).sendTransaction(ArgumentMatchers.any(ZigBeeCommand.class));

        Mockito.doAnswer(new Answer<Future<CommandResult>>() {
            @Override
            public Future<CommandResult> answer(InvocationOnMock invocation) {
                ZigBeeCommand command = (ZigBeeCommand) invocation.getArguments()[0];

                ZigBeeTransactionFuture commandFuture = new ZigBeeTransactionFuture();
                CommandResult result = new CommandResult(responses.get(command.getClusterId()));
                commandFuture.set(result);
                return commandFuture;
            }
        }).when(networkManager).sendTransaction(ArgumentMatchers.any(ZigBeeCommand.class),
                ArgumentMatchers.any(ZigBeeTransactionMatcher.class));

        Mockito.doAnswer(new Answer<Void>() {
            @Override
            public Void answer(InvocationOnMock invocation) {
                Runnable runnable = (Runnable) invocation.getArguments()[0];
                new Thread(runnable).start();
                return null;
            }
        }).when(networkManager).executeTask(ArgumentMatchers.any(Runnable.class));
    }

    @Test
    public void testNormal() {
        // Add all the required responses to a list
        List<Integer> remotes = new ArrayList<>();
        remotes.add(1);
        IeeeAddressResponse ieeeResponse = new IeeeAddressResponse(ZdoStatus.SUCCESS, new IeeeAddress("1234567890ABCDEF"), 0, 0, remotes);
        ieeeResponse.setSourceAddress(new ZigBeeEndpointAddress(0));
        ieeeResponse.setDestinationAddress(new ZigBeeEndpointAddress(0));
        responses.put(ZdoCommandType.IEEE_ADDRESS_REQUEST.getClusterId(), ieeeResponse);

        ZigBeeNetworkDiscoverer discoverer = new ZigBeeNetworkDiscoverer(networkManager);

        ZigBeeNode node0 = Mockito.mock(ZigBeeNode.class);
        Mockito.when(node0.getIeeeAddress()).thenReturn(new IeeeAddress("0000000000000000"));

        ZigBeeNode node1 = Mockito.mock(ZigBeeNode.class);
        Mockito.when(node1.getIeeeAddress()).thenReturn(new IeeeAddress("1111111111111111"));

        Mockito.doAnswer(new Answer<ZigBeeNode>() {
            private int count0 = 0;
            private int count1 = 0;

            @Override
            public ZigBeeNode answer(InvocationOnMock invocation) {
                Object[] args = invocation.getArguments();
                Integer nodeId = (Integer) args[0];

                if (nodeId == 0) {
                    if (count0++ == 0) {
                        return null;
                    }
                    return node0;
                } else {
                    if (count1++ == 0) {
                        return null;
                    }
                    return node1;
                }
            }
        }).when(networkManager).getNode(ArgumentMatchers.anyInt());

        discoverer.setRetryPeriod(Integer.MAX_VALUE);
        discoverer.startup();

        // Check it registers listeners
        Mockito.verify(networkManager).addCommandListener(discoverer);
        Mockito.verify(networkManager).addAnnounceListener(discoverer);

        // Then wait for the nodes to be added
        Mockito.verify(networkManager, Mockito.timeout(TIMEOUT).times(2)).updateNode(nodeCapture.capture());

        ZigBeeNode node = nodeCapture.getValue();
        assertNotNull(node);
        assertEquals(Integer.valueOf(0), node.getNetworkAddress());
        assertEquals(new IeeeAddress("1234567890ABCDEF"), node.getIeeeAddress());
        assertEquals(0, node.getEndpoints().size());

        discoverer.setRetryCount(0);
        discoverer.setRequeryPeriod(0);
        discoverer.shutdown();
    }

    @Test
    public void testNodeAddressUpdate() {
        IeeeAddress ieeeAddress = new IeeeAddress("123456890ABCDEF");

        DeviceAnnounce announce = new DeviceAnnounce(12345, ieeeAddress, null);

        ZigBeeNetworkDiscoverer discoverer = new ZigBeeNetworkDiscoverer(networkManager);
        discoverer.setRetryPeriod(0);
        discoverer.setRequeryPeriod(0);
        discoverer.setRetryCount(0);

        discoverer.commandReceived(announce);
        Mockito.verify(networkManager, Mockito.times(1)).updateNode(ArgumentMatchers.any());

        ZigBeeEndpointAddress address = Mockito.mock(ZigBeeEndpointAddress.class);
        Mockito.when(address.getAddress()).thenReturn(12345);
        ZclCommand zclCommand = Mockito.mock(ZclCommand.class);
        Mockito.when(zclCommand.getSourceAddress()).thenReturn(address);
        discoverer.commandReceived(zclCommand);
    }

    @Test
    public void deviceStatusUpdate() {
        ZigBeeNetworkDiscoverer discoverer = new ZigBeeNetworkDiscoverer(networkManager);
        discoverer.setRetryPeriod(0);
        discoverer.setRequeryPeriod(0);
        discoverer.setRetryCount(0);

        discoverer.deviceStatusUpdate(ZigBeeNodeStatus.UNSECURED_JOIN, 2222, new IeeeAddress("1111111111111111"));

        Mockito.verify(networkManager, Mockito.times(1)).updateNode(ArgumentMatchers.any());
    }

    @Test
    public void rediscoverNodeByEui() throws Exception {
        ZigBeeNetworkDiscoverer discoverer = new ZigBeeNetworkDiscoverer(networkManager);

        IeeeAddressResponse ieeeResponse = new IeeeAddressResponse(ZdoStatus.SUCCESS, new IeeeAddress("1111111111111111"), 1111, null, null);
        ieeeResponse.setSourceAddress(new ZigBeeEndpointAddress(1111));
        ieeeResponse.setDestinationAddress(new ZigBeeEndpointAddress(0));
        responses.put(ZdoCommandType.IEEE_ADDRESS_REQUEST.getClusterId(), ieeeResponse);

        Map<Integer, Long> discoveryStartTime = new HashMap<>();
        discoveryStartTime.put(1111, Long.MAX_VALUE);
        TestUtilities.setField(ZigBeeNetworkDiscoverer.class, discoverer, "initialized", true);
        TestUtilities.setField(ZigBeeNetworkDiscoverer.class, discoverer, "discoveryStartTime", discoveryStartTime);
        discoverer.rediscoverNode(1111);

        Mockito.verify(networkManager, Mockito.timeout(TIMEOUT).times(1)).updateNode(nodeCapture.capture());

        ZigBeeNode node = nodeCapture.getValue();
        assertNotNull(node);
        assertEquals(Integer.valueOf(1111), node.getNetworkAddress());
        assertEquals(new IeeeAddress("1111111111111111"), node.getIeeeAddress());
        assertEquals(0, node.getEndpoints().size());
    }

    @Test
    public void rediscoverNodeByNwk() throws Exception {
        ZigBeeNetworkDiscoverer discoverer = new ZigBeeNetworkDiscoverer(networkManager);

        NetworkAddressResponse nwkResponse = new NetworkAddressResponse(ZdoStatus.SUCCESS, new IeeeAddress("1111111111111111"), 1111, null, null);
        nwkResponse.setSourceAddress(new ZigBeeEndpointAddress(1111));
        nwkResponse.setDestinationAddress(new ZigBeeEndpointAddress(0));
        responses.put(ZdoCommandType.NETWORK_ADDRESS_REQUEST.getClusterId(), nwkResponse);

        Map<Integer, Long> discoveryStartTime = new HashMap<>();
        discoveryStartTime.put(1111, Long.MAX_VALUE);
        TestUtilities.setField(ZigBeeNetworkDiscoverer.class, discoverer, "initialized", true);
        TestUtilities.setField(ZigBeeNetworkDiscoverer.class, discoverer, "discoveryStartTime", discoveryStartTime);
        discoverer.rediscoverNode(new IeeeAddress("1111111111111111"));

        Mockito.verify(networkManager, Mockito.timeout(TIMEOUT).times(1)).updateNode(nodeCapture.capture());

        ZigBeeNode node = nodeCapture.getValue();
        assertNotNull(node);
        assertEquals(Integer.valueOf(1111), node.getNetworkAddress());
        assertEquals(new IeeeAddress("1111111111111111"), node.getIeeeAddress());
        assertEquals(0, node.getEndpoints().size());
    }
}