/**
 * UPnP PortMapper - A tool for managing port forwardings via UPnP
 * Copyright (C) 2015 Christoph Pirkl <christoph at users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.chris.portmapper.router.sbbi;

import static java.util.Arrays.*;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;

import java.io.IOException;
import java.util.HashSet;

import org.chris.portmapper.model.PortMapping;
import org.chris.portmapper.router.RouterException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;

import static org.mockito.Mockito.*;

import net.sbbi.upnp.impls.InternetGatewayDevice;
import net.sbbi.upnp.messages.ActionResponse;
import net.sbbi.upnp.messages.UPNPResponseException;

/**
 * Unit tests for {@link SBBIPortMappingExtractor}.
 */
public class TestPortMappingExtractor {

    @Mock
    private InternetGatewayDevice routerMock;
    @Mock
    private Logger loggerMock;
    private SBBIPortMappingExtractor portMappingExtractor;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        portMappingExtractor = new SBBIPortMappingExtractor(routerMock, 5, loggerMock);
    }

    @Test
    public void allMappingsNull() throws RouterException, IOException, UPNPResponseException {
        simulateUPNPException(5, 713);
        assertEquals(0, portMappingExtractor.getPortMappings().size());
        verify(loggerMock, times(1)).warn(anyString(), anyInt());
        verify(loggerMock, never()).error(anyString());
        assertNumMappingsFound(0, 5);
    }

    @Test
    public void allMappingsNullMaxNumReached() throws RouterException {
        assertEquals(0, portMappingExtractor.getPortMappings().size());
        verify(loggerMock, times(1)).warn(anyString(), anyInt());
        verify(loggerMock, never()).error(anyString());
        assertNumMappingsFound(0, 5);
    }

    @Test
    public void noMapping() throws RouterException, IOException, UPNPResponseException {
        simulateUPNPException(0, 713);
        assertEquals(0, portMappingExtractor.getPortMappings().size());
        assertNoWarningOrErrorLogged();
        assertNumMappingsFound(0, 0);
    }

    @Test
    public void wrongErrorCode() throws RouterException, IOException, UPNPResponseException {
        simulateUPNPException(0, 42);
        assertEquals(0, portMappingExtractor.getPortMappings().size());
        verify(loggerMock, never()).warn(anyString());
        verify(loggerMock, never()).error(anyString());
        verify(loggerMock, never()).warn(anyString(), any(Throwable.class));
        verify(loggerMock).error(anyString(), any(Throwable.class));
        assertNumMappingsFound(0, 0);
    }

    @Test
    public void oneMapping() throws RouterException, IOException, UPNPResponseException {
        simulateMapping(0);
        simulateUPNPException(1, 713);
        assertEquals(1, portMappingExtractor.getPortMappings().size());
        assertNoWarningOrErrorLogged();
        assertNumMappingsFound(1, 0);
    }

    private void assertNumMappingsFound(final int numFound, final int numNull) {
        verify(loggerMock).debug("Found {} mappings, {} mappings returned as null.", numFound, numNull);
    }

    private void assertNoWarningOrErrorLogged() {
        verify(loggerMock, never()).warn(anyString());
        verify(loggerMock, never()).error(anyString());
        verify(loggerMock, never()).warn(anyString(), any(Throwable.class));
        verify(loggerMock, never()).error(anyString(), any(Throwable.class));
    }

    private void simulateMapping(final int mappingEntry) throws IOException, UPNPResponseException {
        final ActionResponse response = mock(ActionResponse.class);
        when(response.getOutActionArgumentNames()).thenReturn(
                new HashSet<Object>(asList(PortMapping.MAPPING_ENTRY_ENABLED, PortMapping.MAPPING_ENTRY_EXTERNAL_PORT,
                        PortMapping.MAPPING_ENTRY_INTERNAL_CLIENT, PortMapping.MAPPING_ENTRY_INTERNAL_PORT,
                        PortMapping.MAPPING_ENTRY_LEASE_DURATION, PortMapping.MAPPING_ENTRY_PORT_MAPPING_DESCRIPTION,
                        PortMapping.MAPPING_ENTRY_PROTOCOL, PortMapping.MAPPING_ENTRY_REMOTE_HOST)));
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_ENABLED)).thenReturn("1");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_EXTERNAL_PORT)).thenReturn("2");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_INTERNAL_CLIENT)).thenReturn("internal");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_INTERNAL_PORT)).thenReturn("3");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_LEASE_DURATION)).thenReturn("4");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_PORT_MAPPING_DESCRIPTION))
                .thenReturn("description");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_PROTOCOL)).thenReturn("TCP");
        when(response.getOutActionArgumentValue(PortMapping.MAPPING_ENTRY_REMOTE_HOST)).thenReturn("remote");
        when(routerMock.getGenericPortMappingEntry(mappingEntry)).thenReturn(response);
    }

    private void simulateUPNPException(final int mappingEntry, final int errorCode)
            throws IOException, UPNPResponseException {
        when(routerMock.getGenericPortMappingEntry(mappingEntry)).thenThrow(new UPNPResponseException(errorCode,
                "exception for entry " + mappingEntry + ", error code " + errorCode));
    }
}
