/*
 * 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.
 */

using System;
using Apache.Qpid.Proton.Engine.Exceptions;
using Apache.Qpid.Proton.Engine.Sasl;
using Apache.Qpid.Proton.Engine.Sasl.Client;
using Apache.Qpid.Proton.Test.Driver;
using Apache.Qpid.Proton.Types.Security;
using NUnit.Framework;

namespace Apache.Qpid.Proton.Engine.Implementation
{
   [TestFixture, Timeout(20000)]
   public class ProtonSaslClientTest : ProtonEngineTestSupport
   {
      [Test]
      public void TestSaslAnonymousConnection()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         peer.ExpectSASLAnonymousConnect();
         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(null, null);

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestDriverThrowsIfServerStateRequestedAfterClientStateActivated()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         peer.ExpectSASLAnonymousConnect();
         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(null, null);

         Assert.Throws<InvalidOperationException>(() => engine.SaslDriver.Server());

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestSaslAnonymousConnectionWhenPlainAlsoOfferedButNoCredentialsGiven()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         peer.ExpectSASLHeader().RespondWithSASLHeader();
         peer.RemoteSaslMechanisms().WithMechanisms("PLAIN", "ANONYMOUS").Queue();
         peer.ExpectSaslInit().WithMechanism("ANONYMOUS");
         peer.RemoteSaslOutcome().WithCode(Test.Driver.Codec.Security.SaslCode.Ok).Queue();
         peer.ExpectAMQPHeader().RespondWithAMQPHeader();
         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(null, null);

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestSaslPlainConnection()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         // Expect a PLAIN connection
         String user = "user";
         String pass = "qwerty123456";

         peer.ExpectSASLPlainConnect(user, pass);
         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(user, pass);

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestSaslPlainConnectionWhenUnknownMechanismsOfferedBeforeIt()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         // Expect a PLAIN connection
         String user = "user";
         String pass = "qwerty123456";

         peer.ExpectSASLHeader().RespondWithSASLHeader();
         peer.RemoteSaslMechanisms().WithMechanisms("UNKNOWN", "PLAIN", "ANONYMOUS").Queue();
         peer.ExpectSaslInit().WithMechanism("PLAIN").WithInitialResponse(peer.SaslPlainInitialResponse(user, pass));
         peer.RemoteSaslOutcome().WithCode(Test.Driver.Codec.Security.SaslCode.Ok).Queue();
         peer.ExpectAMQPHeader().RespondWithAMQPHeader();

         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(user, pass);

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestDefaultClientSaslMismatchBetweenClientAndServer()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         peer.ExpectSASLHeader().RespondWithSASLHeader();
         peer.RemoteSaslMechanisms().WithMechanisms("PLAIN").Queue();

         // Default client only know about ANONYMOUS
         engine.SaslDriver.Client();

         IConnection connection = engine.Start().Open();

         try
         {
            connection.Close();
            Assert.Fail("Engine should have failed");
         }
         catch (EngineFailedException)
         {
            // Expected as engine failed but was not shutdown
         }

         peer.WaitForScriptToComplete();

         Assert.IsTrue(engine.IsFailed);
         Assert.IsNotNull(failure);
      }

      [Test]
      public void TestSaslXOauth2Connection()
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         // Expect a XOAUTH2 connection
         String user = "user";
         String pass = "eyB1c2VyPSJ1c2VyIiB9";

         peer.ExpectSaslXOauth2Connect(user, pass);
         peer.ExpectOpen().Respond();
         peer.ExpectClose().Respond();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator(user, pass);

         IConnection connection = engine.Start().Open();

         connection.Close();

         peer.WaitForScriptToComplete();

         Assert.IsNull(failure);
      }

      [Test]
      public void TestSaslFailureCodesFailEngine()
      {
         DoSaslFailureCodesTestImpl(SaslCode.Auth);
         DoSaslFailureCodesTestImpl(SaslCode.Sys);
         DoSaslFailureCodesTestImpl(SaslCode.SysPerm);
         DoSaslFailureCodesTestImpl(SaslCode.SysTemp);
      }

      private void DoSaslFailureCodesTestImpl(SaslCode saslFailureCode)
      {
         IEngine engine = IEngineFactory.Proton.CreateEngine();
         engine.ErrorHandler((result) => failure = result.FailureCause);
         ProtonTestConnector peer = CreateTestPeer(engine);

         peer.ExpectSASLHeader().RespondWithSASLHeader();
         peer.RemoteSaslMechanisms().WithMechanisms("PLAIN", "ANONYMOUS").Queue();
         peer.ExpectSaslInit().WithMechanism("PLAIN");
         peer.RemoteSaslOutcome().WithCode(((byte)saslFailureCode)).Queue();

         engine.SaslDriver.Client().Authenticator = CreateSaslPlainAuthenticator("user", "pass");

         engine.Start().Open();

         peer.WaitForScriptToComplete();

         Assert.IsNotNull(failure);
         Assert.IsFalse(engine.IsShutdown);
         Assert.IsTrue(engine.IsFailed);
         Assert.AreEqual(failure, engine.FailureCause);
         Assert.IsTrue(failure is SaslException);
      }

      private SaslAuthenticator CreateSaslPlainAuthenticator(string user, string password)
      {
         ISaslCredentialsProvider credentials =
            new DelegatedSaslCredentialsProvider().UsernameSupplier(() => user)
                                                  .PasswordSupplier(() => password);

         return new SaslAuthenticator(credentials);
      }
   }
}