using Renci.SshNet.Common;
using Renci.SshNet.IntegrationTests.Common;

namespace Renci.SshNet.IntegrationTests
{
    [TestClass]
    public class AuthenticationTests : IntegrationTestBase
    {
        private AuthenticationMethodFactory _authenticationMethodFactory;
        private IConnectionInfoFactory _connectionInfoFactory;
        private IConnectionInfoFactory _adminConnectionInfoFactory;
        private RemoteSshdConfig _remoteSshdConfig;

        [TestInitialize]
        public void SetUp()
        {
            _authenticationMethodFactory = new AuthenticationMethodFactory();
            _connectionInfoFactory = new LinuxVMConnectionFactory(SshServerHostName, SshServerPort, _authenticationMethodFactory);
            _adminConnectionInfoFactory = new LinuxAdminConnectionFactory(SshServerHostName, SshServerPort);
            _remoteSshdConfig = new RemoteSshd(_adminConnectionInfoFactory).OpenConfig();
        }

        [TestCleanup]
        public void TearDown()
        {
            _remoteSshdConfig?.Reset();

            using (var client = new SshClient(_adminConnectionInfoFactory.Create()))
            {
                client.Connect();

                // Reset the password back to the "regular" password.
                using (var cmd = client.RunCommand($"echo \"{Users.Regular.Password}\n{Users.Regular.Password}\" | sudo passwd " + Users.Regular.UserName))
                {
                    Assert.AreEqual(0, cmd.ExitStatus, cmd.Error);
                }

                // Remove password expiration
                using (var cmd = client.RunCommand($"sudo chage --expiredate -1 " + Users.Regular.UserName))
                {
                    Assert.AreEqual(0, cmd.ExitStatus, cmd.Error);
                }
            }
        }

        [TestMethod]
        public void Multifactor_PublicKey()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        [TestCategory("Authentication")]
        public void Multifactor_PublicKey_Connect_Then_Reconnect()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
                client.Disconnect();
                client.Connect();
                client.Disconnect();
            }
        }

        [TestMethod]
        public void Multifactor_PublicKeyWithPassPhrase()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyWithPassPhraseAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_PublicKey_MultiplePrivateKey()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserMultiplePrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_PublicKey_MultipleAuthenticationMethod()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_KeyboardInteractiveAndPublicKey()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive,publickey")
                             .WithChallengeResponseAuthentication(true)
                             .WithKeyboardInteractiveAuthentication(true)
                             .WithUsePAM(true)
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(),
                                                               _authenticationMethodFactory.CreateRegularUserKeyboardInteractiveAuthenticationMethod(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_Password_ExceedsPartialSuccessLimit()
        {
            // configure server to require more successfull authentications from a given method than our partial
            // success limit (5) allows
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password,password,password,password,password")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                try
                {
                    client.Connect();
                    Assert.Fail();
                }
                catch (SshAuthenticationException ex)
                {
                    Assert.IsNull(ex.InnerException);
                    Assert.AreEqual("Reached authentication attempt limit for method (password).", ex.Message);
                }
            }
        }

        [TestMethod]
        public void Multifactor_Password_MatchPartialSuccessLimit()
        {
            // configure server to require a number of successfull authentications from a given method that exactly
            // matches our partial success limit (5)

            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password,password,password,password")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_Password_Or_PublicKeyAndKeyboardInteractive()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,keyboard-interactive")
                             .WithChallengeResponseAuthentication(true)
                             .WithKeyboardInteractiveAuthentication(true)
                             .WithUsePAM(true)
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod(),
                                                               _authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void Multifactor_Password_Or_PublicKeyAndPassword_BadPassword()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password publickey,password")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                try
                {
                    client.Connect();
                    Assert.Fail();
                }
                catch (SshAuthenticationException ex)
                {
                    Assert.IsNull(ex.InnerException);
                    Assert.AreEqual("Permission denied (password).", ex.Message);
                }
            }
        }

        [TestMethod]
        public void Multifactor_PasswordAndPublicKey_Or_PasswordAndPassword()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,publickey password,password")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }

            connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethodWithBadPassword(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                try
                {
                    client.Connect();
                    Assert.Fail();
                }
                catch (SshAuthenticationException ex)
                {
                    Assert.IsNull(ex.InnerException);
                    Assert.AreEqual("Permission denied (password).", ex.Message);
                }
            }

        }

        [TestMethod]
        public void Multifactor_PasswordAndPassword_Or_PublicKey()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password,password publickey")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod(),
                                                               _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }

            connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }

        }

        [TestMethod]
        public void Multifactor_Password_Or_Password()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "password password")
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }

            connectionInfo = _connectionInfoFactory.Create(_authenticationMethodFactory.CreateRegularUserPasswordAuthenticationMethod(),
                                                           _authenticationMethodFactory.CreateRegularUserPrivateKeyAuthenticationMethodWithBadKey());
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
            }
        }

        [TestMethod]
        public void KeyboardInteractive_PasswordExpired()
        {
            var temporaryPassword = new Random().Next().ToString();

            using (var client = new SshClient(_adminConnectionInfoFactory.Create()))
            {
                client.Connect();

                // Temporarity modify password so that when we expire this password, we change reset the password back to
                // the "regular" password.
                using (var cmd = client.RunCommand($"echo \"{temporaryPassword}\n{temporaryPassword}\" | sudo passwd " + Users.Regular.UserName))
                {
                    Assert.AreEqual(0, cmd.ExitStatus, cmd.Error);
                }

                // Force the password to expire immediately
                using (var cmd = client.RunCommand($"sudo chage -d 0 " + Users.Regular.UserName))
                {
                    Assert.AreEqual(0, cmd.ExitStatus, cmd.Error);
                }
            }

            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive")
                             .WithChallengeResponseAuthentication(true)
                             .WithKeyboardInteractiveAuthentication(true)
                             .WithUsePAM(true)
                             .Update()
                             .Restart();

            var keyboardInteractive = new KeyboardInteractiveAuthenticationMethod(Users.Regular.UserName);
            int authenticationPromptCount = 0;
            keyboardInteractive.AuthenticationPrompt += (sender, args) =>
            {
                Console.WriteLine(args.Instruction);
                foreach (var authenticationPrompt in args.Prompts)
                {
                    Console.WriteLine(authenticationPrompt.Request);
                    switch (authenticationPromptCount)
                    {
                        case 0:
                            // Regular password prompt
                            authenticationPrompt.Response = temporaryPassword;
                            break;
                        case 1:
                            // Password expired, provide current password
                            authenticationPrompt.Response = temporaryPassword;
                            break;
                        case 2:
                            // Password expired, provide new password
                            authenticationPrompt.Response = Users.Regular.Password;
                            break;
                        case 3:
                            // Password expired, retype new password
                            authenticationPrompt.Response = Users.Regular.Password;
                            break;
                    }

                    authenticationPromptCount++;
                }
            };

            var connectionInfo = _connectionInfoFactory.Create(keyboardInteractive);
            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();
                Assert.AreEqual(4, authenticationPromptCount);
            }
        }

        [TestMethod]
        public void KeyboardInteractiveConnectionInfo()
        {
            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive")
                             .WithChallengeResponseAuthentication(true)
                             .WithKeyboardInteractiveAuthentication(true)
                             .WithUsePAM(true)
                             .Update()
                             .Restart();

            var host = SshServerHostName;
            var port = SshServerPort;
            var username = User.UserName;
            var password = User.Password;

            #region Example KeyboardInteractiveConnectionInfo AuthenticationPrompt

            var connectionInfo = new KeyboardInteractiveConnectionInfo(host, port, username);
            connectionInfo.AuthenticationPrompt += delegate (object sender, AuthenticationPromptEventArgs e)
                                                       {
                                                           Console.WriteLine(e.Instruction);

                                                           foreach (var prompt in e.Prompts)
                                                           {
                                                               Console.WriteLine(prompt.Request);
                                                               prompt.Response = password;
                                                           }
                                                       };

            using (var client = new SftpClient(connectionInfo))
            {
                client.Connect();

                //  Do something here
                client.Disconnect();
            }

            #endregion

            Assert.AreEqual(connectionInfo.Host, SshServerHostName);
            Assert.AreEqual(connectionInfo.Username, User.UserName);
        }

        [TestMethod]
        public void KeyboardInteractive_NoResponseSet_ThrowsSshAuthenticationException()
        {
            // ...instead of a cryptic ArgumentNullException
            // https://github.com/sshnet/SSH.NET/issues/382

            _remoteSshdConfig.WithAuthenticationMethods(Users.Regular.UserName, "keyboard-interactive")
                             .WithChallengeResponseAuthentication(true)
                             .WithKeyboardInteractiveAuthentication(true)
                             .WithUsePAM(true)
                             .Update()
                             .Restart();

            var connectionInfo = _connectionInfoFactory.Create(new KeyboardInteractiveAuthenticationMethod(Users.Regular.UserName));

            using (var client = new SftpClient(connectionInfo))
            {
                try
                {
                    client.Connect();
                    Assert.Fail();
                }
                catch (SshAuthenticationException ex)
                {
                    Assert.IsNull(ex.InnerException);
                    Assert.IsTrue(ex.Message.StartsWith("AuthenticationPrompt.Response is null for prompt \"Password: \""), $"Message was \"{ex.Message}\"");
                }
            }
        }
    }
}
