/*
 * SFTPEnvironmentTest.java
 * Copyright 2017 Rob Spoor
 *
 * 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 com.github.robtimus.filesystems.sftp;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collections;
import java.util.Properties;
import java.util.UUID;
import java.util.Vector;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import com.github.robtimus.filesystems.Messages;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.HostKey;
import com.jcraft.jsch.HostKeyRepository;
import com.jcraft.jsch.IdentityRepository;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Proxy;
import com.jcraft.jsch.ProxyHTTP;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SocketFactory;
import com.jcraft.jsch.UserInfo;

@SuppressWarnings({ "nls", "javadoc" })
public class SFTPEnvironmentTest {

    @Test
    public void testWithConfig() {
        SFTPEnvironment env = new SFTPEnvironment();

        assertEquals(Collections.emptyMap(), env);

        String key1 = "key1";
        String value1 = "value1";
        String key2 = "key2";
        String value2 = "value2";

        env.withConfig(key1, value1);
        env.withConfig(key2, value2);

        Properties properties = new Properties();
        properties.setProperty(key1, value1);
        properties.setProperty(key2, value2);

        assertEquals(Collections.singletonMap("config", properties), env);
    }

    @Test
    public void testWithIdentity() {
        SFTPEnvironment env = new SFTPEnvironment();

        assertEquals(Collections.emptyMap(), env);

        Identity identity = IdentityTest.fromFiles();

        env.withIdentity(identity);

        assertEquals(Collections.singletonMap("identities", Arrays.asList(identity)), env);
    }

    @Test
    public void testWithIdentities() {
        SFTPEnvironment env = new SFTPEnvironment();

        assertEquals(Collections.emptyMap(), env);

        Identity identity1 = IdentityTest.fromFiles();
        Identity identity2 = IdentityTest.fromData();

        env.withIdentities(identity1, identity2);

        assertEquals(Collections.singletonMap("identities", Arrays.asList(identity1, identity2)), env);
    }

    @Test
    public void testInitializeJSchEmpty() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();

        JSch jsch = mock(JSch.class);
        env.initialize(jsch);

        verifyNoMoreInteractions(jsch);
    }

    @Test
    public void testInitializeJSchFull() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        JSch jsch = mock(JSch.class);
        env.initialize(jsch);

        verify(jsch).setIdentityRepository((IdentityRepository) env.get("identityRepository"));
        IdentityTest.assertIdentityFromFilesAdded(jsch);
        verify(jsch).setHostKeyRepository((HostKeyRepository) env.get("hostKeyRepository"));
        verify(jsch).setKnownHosts(((File) env.get("knownHosts")).getAbsolutePath());
        verifyNoMoreInteractions(jsch);
    }

    @Test
    public void testInitializeJSchWithNulls() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeWithNulls(env);

        JSch jsch = mock(JSch.class);
        env.initialize(jsch);

        verify(jsch).setIdentityRepository(null);
        verify(jsch).setHostKeyRepository(null);
        verifyNoMoreInteractions(jsch);
    }

    @Test
    public void testInitializeJSchWithNullIdentity() {
        final SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);
        env.put("identities", Collections.singleton(null));

        final JSch jsch = mock(JSch.class);
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> env.initialize(jsch));
        assertEquals(Messages.fileSystemProvider().env().invalidProperty("identities", env.get("identities")).getMessage(), exception.getMessage());
    }

    @Test
    public void testInitializeJSchWithInvalidIdentity() {
        final SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);
        env.put("identities", Collections.singleton("foobar"));

        final JSch jsch = mock(JSch.class);
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> env.initialize(jsch));
        assertEquals(Messages.fileSystemProvider().env().invalidProperty("identities", env.get("identities")).getMessage(), exception.getMessage());
    }

    @Test
    public void testInitializeSessionEmpty() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();

        Session session = mock(Session.class);
        env.initialize(session);

        verifyNoMoreInteractions(session);
    }

    @Test
    public void testInitializeSessionFull() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        Session session = mock(Session.class);
        env.initialize(session);

        verify(session).setProxy((Proxy) env.get("proxy"));
        verify(session).setUserInfo((UserInfo) env.get("userInfo"));
        verify(session).setPassword(new String((char[]) env.get("password")));
        verify(session).setConfig((Properties) env.get("config"));
        verify(session).setSocketFactory((SocketFactory) env.get("socketFactory"));
        verify(session).setTimeout((int) env.get("timeOut"));
        verify(session).setClientVersion((String) env.get("clientVersion"));
        verify(session).setHostKeyAlias((String) env.get("hostKeyAlias"));
        verify(session).setServerAliveInterval((int) env.get("serverAliveInterval"));
        verify(session).setServerAliveCountMax((int) env.get("serverAliveCountMax"));
        verifyNoMoreInteractions(session);
    }

    @Test
    public void testInitializeSessionWithNulls() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeWithNulls(env);

        Session session = mock(Session.class);
        env.initialize(session);

        verify(session).setProxy(null);
        verify(session).setUserInfo(null);
        verify(session).setPassword((String) null);
        verify(session).setSocketFactory(null);
        verify(session).setClientVersion(null);
        verify(session).setHostKeyAlias(null);
        verifyNoMoreInteractions(session);
    }

    @Test
    public void testConnectSessionEmpty() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();

        Session session = mock(Session.class);
        env.connect(session);

        verify(session).connect();
        verify(session).openChannel("sftp");
        verifyNoMoreInteractions(session);
    }

    @Test
    public void testConnectSessionFull() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        Session session = mock(Session.class);
        env.connect(session);

        verify(session).connect((int) env.get("connectTimeout"));
        verify(session).openChannel("sftp");
        verifyNoMoreInteractions(session);
    }

    @Test
    public void testInitializeChannelPreConnectEmpty() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePreConnect(channel);

        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testInitializeChannelPreConnectFull() throws IOException, SftpException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePreConnect(channel);

        verify(channel).setAgentForwarding((boolean) env.get("agentForwarding"));
        verify(channel).setFilenameEncoding((String) env.get("filenameEncoding"));
        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testInitializeChannelPreConnectWithNulls() throws IOException, SftpException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeWithNulls(env);

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePreConnect(channel);

        verify(channel).setFilenameEncoding(null);
        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testConnectChannelEmpty() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();

        ChannelSftp channel = mock(ChannelSftp.class);

        env.connect(channel);

        verify(channel).connect();
        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testConnectChannelFull() throws IOException, JSchException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        ChannelSftp channel = mock(ChannelSftp.class);

        env.connect(channel);

        verify(channel).connect((int) env.get("connectTimeout"));
        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testInitializeChannelPostConnectEmpty() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePostConnect(channel);

        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testInitializeChannelPostConnectFull() throws IOException, SftpException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePostConnect(channel);

        verify(channel).cd((String) env.get("defaultDir"));
        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testInitializeChannelPostConnectWithNulls() throws IOException {
        SFTPEnvironment env = new SFTPEnvironment();
        initializeWithNulls(env);

        ChannelSftp channel = mock(ChannelSftp.class);

        env.initializePostConnect(channel);

        verifyNoMoreInteractions(channel);
    }

    @Test
    public void testSessionHostKeyRepository() throws JSchException, IOException, ReflectiveOperationException {
        testSessionPropertyInheritedFromJSch("getHostKeyRepository", "hostKeyRepository");
    }

    @Test
    public void testSessionIdentityRepository() throws JSchException, IOException, ReflectiveOperationException {
        testSessionPropertyInheritedFromJSch("getIdentityRepository", "identityRepository");
    }

    private void testSessionPropertyInheritedFromJSch(String getterName, String propertyName)
            throws IOException, JSchException, ReflectiveOperationException {

        SFTPEnvironment env = new SFTPEnvironment();
        initializeFully(env);
        // by adding an identity, the identity repository gets overwritten by a wrapper
        env.remove("identities");

        JSch jsch = new JSch();
        env.initialize(jsch);

        Session session = jsch.getSession("localhost");
        env.initialize(session);

        Method method = Session.class.getDeclaredMethod(getterName);
        method.setAccessible(true);

        assertEquals(env.get(propertyName), method.invoke(session));
    }

    @Test
    public void testWithClientConnectionWaitTimeoutWithUnit() {
        SFTPEnvironment env = new SFTPEnvironment();

        assertEquals(Collections.emptyMap(), env);

        env.withClientConnectionWaitTimeout(1, TimeUnit.MINUTES);

        assertEquals(Collections.singletonMap("clientConnectionWaitTimeout", 60_000L), env);
    }

    private void initializeFully(SFTPEnvironment env) {
        env.withUsername(UUID.randomUUID().toString());
        env.withConnectTimeout(1000);
        env.withProxy(new ProxyHTTP("localhost"));
        env.withUserInfo(new SimpleUserInfo(UUID.randomUUID().toString().toCharArray()));
        env.withPassword(UUID.randomUUID().toString().toCharArray());
        env.withConfig(System.getProperties());
        env.withSocketFactory(new TestSocketFactory());
        env.withTimeout(1000);
        env.withClientVersion("SSH-2");
        env.withHostKeyAlias("alias");
        env.withServerAliveInterval(500);
        env.withServerAliveCountMax(5);
        env.withIdentityRepository(new TestIdentityRepository());
        env.withIdentity(IdentityTest.fromFiles());
        env.withHostKeyRepository(new TestHostKeyRepository());
        env.withKnownHosts(new File("known_hosts"));
        env.withAgentForwarding(false);
        env.withFilenameEncoding("UTF-8");
        env.withDefaultDirectory("/");
        env.withClientConnectionCount(5);
        env.withFileSystemExceptionFactory(DefaultFileSystemExceptionFactory.INSTANCE);
    }

    private void initializeWithNulls(SFTPEnvironment env) {
        env.withUsername(null);
        env.withProxy(null);
        env.withUserInfo(null);
        env.withPassword(null);
        env.withSocketFactory(null);
        env.withClientVersion(null);
        env.withHostKeyAlias(null);
        env.withIdentityRepository(null);
        env.withHostKeyRepository(null);
        env.withFilenameEncoding(null);
        env.withDefaultDirectory(null);
        env.withFileSystemExceptionFactory(null);
    }

    static final class TestSocketFactory implements SocketFactory {

        @Override
        public Socket createSocket(String host, int port) {
            return null;
        }

        @Override
        public InputStream getInputStream(Socket socket) {
            return null;
        }

        @Override
        public OutputStream getOutputStream(Socket socket) {
            return null;
        }
    }

    static final class TestIdentityRepository implements IdentityRepository {

        @Override
        public String getName() {
            return null;
        }

        @Override
        public int getStatus() {
            return 0;
        }

        @Override
        public Vector<?> getIdentities() {
            return null;
        }

        @Override
        public boolean add(byte[] identity) {
            return false;
        }

        @Override
        public boolean remove(byte[] blob) {
            return false;
        }

        @Override
        public void removeAll() {
            // does nothing
        }
    }

    static final class TestHostKeyRepository implements HostKeyRepository {

        @Override
        public int check(String host, byte[] key) {
            return 0;
        }

        @Override
        public void add(HostKey hostkey, UserInfo ui) {
            // does nothing
        }

        @Override
        public void remove(String host, String type) {
            // does nothing
        }

        @Override
        public void remove(String host, String type, byte[] key) {
            // does nothing
        }

        @Override
        public String getKnownHostsRepositoryID() {
            return null;
        }

        @Override
        public HostKey[] getHostKey() {
            return null;
        }

        @Override
        public HostKey[] getHostKey(String host, String type) {
            return null;
        }
    }
}