package org.jboss.resteasy.test.client.exception;

import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;

import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.ext.Provider;

import org.apache.http.ConnectionClosedException;
import org.apache.http.HttpException;
import org.apache.http.MalformedChunkCodingException;
import org.apache.http.MethodNotSupportedException;
import org.apache.http.NoHttpResponseException;
import org.apache.http.ProtocolException;
import org.apache.http.UnsupportedHttpVersionException;
import org.apache.http.auth.AuthenticationException;
import org.apache.http.auth.InvalidCredentialsException;
import org.apache.http.auth.MalformedChallengeException;
import org.apache.http.client.CircularRedirectException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.NonRepeatableRequestException;
import org.apache.http.client.RedirectException;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.http.conn.ConnectionPoolTimeoutException;
import org.apache.http.conn.HttpHostConnectException;
import org.apache.http.cookie.CookieRestrictionViolationException;
import org.apache.http.cookie.MalformedCookieException;
import org.apache.http.impl.auth.NTLMEngineException;
import org.apache.http.impl.client.TunnelRefusedException;
import org.jboss.resteasy.client.ClientExecutor;
import org.jboss.resteasy.client.ClientRequest;
import org.jboss.resteasy.client.ClientResponse;
import org.jboss.resteasy.client.ClientResponseFailure;
import org.jboss.resteasy.client.ProxyBuilder;
import org.jboss.resteasy.client.exception.ResteasyAuthenticationException;
import org.jboss.resteasy.client.exception.ResteasyCircularRedirectException;
import org.jboss.resteasy.client.exception.ResteasyClientProtocolException;
import org.jboss.resteasy.client.exception.ResteasyConnectTimeoutException;
import org.jboss.resteasy.client.exception.ResteasyConnectionClosedException;
import org.jboss.resteasy.client.exception.ResteasyConnectionPoolTimeoutException;
import org.jboss.resteasy.client.exception.ResteasyCookieRestrictionViolationException;
import org.jboss.resteasy.client.exception.ResteasyHttpException;
import org.jboss.resteasy.client.exception.ResteasyHttpHostConnectException;
import org.jboss.resteasy.client.exception.ResteasyIOException;
import org.jboss.resteasy.client.exception.ResteasyInvalidCredentialsException;
import org.jboss.resteasy.client.exception.ResteasyMalformedChallengeException;
import org.jboss.resteasy.client.exception.ResteasyMalformedChunkCodingException;
import org.jboss.resteasy.client.exception.ResteasyMalformedCookieException;
import org.jboss.resteasy.client.exception.ResteasyMethodNotSupportedException;
import org.jboss.resteasy.client.exception.ResteasyNTLMEngineException;
import org.jboss.resteasy.client.exception.ResteasyNoHttpResponseException;
import org.jboss.resteasy.client.exception.ResteasyNonRepeatableRequestException;
import org.jboss.resteasy.client.exception.ResteasyProtocolException;
import org.jboss.resteasy.client.exception.ResteasyRedirectException;
import org.jboss.resteasy.client.exception.ResteasyTunnelRefusedException;
import org.jboss.resteasy.client.exception.ResteasyUnsupportedHttpVersionException;
import org.jboss.resteasy.client.exception.mapper.ApacheHttpClient4ExceptionMapper;
import org.jboss.resteasy.client.exception.mapper.ClientExceptionMapper;
import org.jboss.resteasy.core.Dispatcher;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.jboss.resteasy.test.EmbeddedContainer;
import org.jboss.resteasy.util.Types;
import org.junit.Test;

/**
 * @author <a href="ron.sigal@jboss.com">Ron Sigal</a>
 * @version $Revision: 1.1 $
 *
 * Copyright Jul 28, 2012
 */
public class HttpClient4ClientExceptionMapperTest
{
   protected static ResteasyDeployment deployment;
   protected static Dispatcher dispatcher;

   public static interface Foo
   {
      @GET
      @Path("foo")
      String getFoo();

      @PUT
      @Path("foo")
      String setFoo(String value);
      
      @POST
      @Path("error")
      String error();
   }

   @Path("foo")
   public static class TestResource implements Foo
   {
      @Context
      HttpRequest request;

      @Override
      public String getFoo()
      {
         return request.getHttpHeaders().getAcceptableMediaTypes().toString();
      }

      @Override
      public String setFoo(String value)
      {
         return request.getHttpHeaders().getMediaType().toString();
      }

      @Override
      public String error()
      {
         throw new WebApplicationException(500);
      }
   }

   public void before() throws Exception
   {
      deployment = EmbeddedContainer.start();
      dispatcher = deployment.getDispatcher();
      deployment.getRegistry().addPerRequestResource(TestResource.class);
   }
   
   public void beforeProviderInstance(ClientExceptionMapper<?> mapper) throws Exception
   {
      deployment = new ResteasyDeployment();
      ArrayList<Object> providers = new ArrayList<Object>();
      providers.add(mapper);
      deployment.setProviders(providers);
      deployment.start();
      deployment.getRegistry().addPerRequestResource(TestResource.class);
   }
   
   public void beforeApplicationClasses(final Class<?> mapper) throws Exception
   {
      deployment = new ResteasyDeployment();
      Application application = new Application()
      {
         public Set<Class<?>> getClasses()
         {
            HashSet<Class<?>> classes = new HashSet<Class<?>>();
            classes.add(mapper);
            return classes;
         }
      };
      deployment.setApplication(application);
      deployment.start();
      deployment.getRegistry().addPerRequestResource(TestResource.class);
   }
   
   public void beforeApplicationSingleton(final ClientExceptionMapper<?> mapper) throws Exception
   {
      deployment = new ResteasyDeployment();
      Application application = new Application()
      {
         public Set<Object> getSingletons()
         {
            HashSet<Object> singletons = new HashSet<Object>();
            singletons.add(mapper);
            return singletons;
         }
      };
      deployment.setApplication(application);
      deployment.start();
      deployment.getRegistry().addPerRequestResource(TestResource.class);
   }
   
   public void beforeClassNames(final Class<?> mapper) throws Exception
   {
      deployment = new ResteasyDeployment();
      ArrayList<String> providerClasses = new ArrayList<String>();
      providerClasses.add(mapper.getName());
      deployment.setProviderClasses(providerClasses);
      deployment.start();
      deployment.getRegistry().addPerRequestResource(TestResource.class);
   }
   
   public void beforeContextParams(final Class<?> mapper) throws Exception
   {      
      Hashtable<String,String> initParams = new Hashtable<String,String>();
      Hashtable<String,String> contextParams = new Hashtable<String,String>();
      contextParams.put("javax.ws.rs.Application", TestApplication.class.getName());
      deployment = EmbeddedContainer.start(initParams, contextParams);
   }
   
   public void beforeInitParams(final Class<?> mapper) throws Exception
   {      
      Hashtable<String,String> initParams = new Hashtable<String,String>();
      Hashtable<String,String> contextParams = new Hashtable<String,String>();
      initParams.put("javax.ws.rs.Application", TestApplication.class.getName());
      deployment = EmbeddedContainer.start(initParams, contextParams);
   }

   public void after() throws Exception
   {
      EmbeddedContainer.stop();
      deployment = null;
   }

   /**
    * Verify that ClientResponseFailure is thrown if the request successfully invokes a
    * resource method, and the resource method returns a status code >= 400.  That is,
    * verify that the default error handling mechanism still works.
    */
   @Test
   public void testClientResponseFailure() throws Exception
   {
      before();
      boolean ok = false;
      try
      {
         Foo foo = ProxyBuilder.build(Foo.class, "http://localhost:8081/foo/").serverMediaType(MediaType.TEXT_PLAIN_TYPE).now();
         String answer = foo.error();
         System.out.println("answer: " + answer);
      }
      catch (ClientResponseFailure e)
      {
         ok = true;
      }
      catch (Throwable t)
      {
         traverseException(t);
         fail("Expected ClientResponseFailure, got " + t);
      }
      finally
      {
         after();
      }
      assertTrue("Expected ClientResponseFailure, got no Exception", ok);
   }
   
   @Test
   public void testApacheHttpClient4Executor() throws Exception
   {
      doTestApacheHttpClient4Executor();
   }

   /**
    * Verify that ApacheHttpClient4ExceptionMapper always gets set.
    */
   @Test
   public void testApacheHttpClient4ExecutorAgain() throws Exception
   {
      doTestApacheHttpClient4Executor();
   }

   private void doTestApacheHttpClient4Executor() throws Exception
   {
      before();
      try
      {
         Foo foo = ProxyBuilder.build(Foo.class, "http://localhost:9999").serverMediaType(MediaType.TEXT_PLAIN_TYPE).now();
         foo.getFoo();
      }
      catch (ResteasyIOException e)
      {
         traverseException(e);
         assertTrue(HttpHostConnectException.class.equals(e.getCause().getClass()));
      }
      catch (Throwable t)
      {
         traverseException(t);
         fail("Expected HttpHostConnectException, got " + t);
      }
      finally
      {
         after();
      }
   }

   @Test
   public void testAuthenticationException() throws Exception
   {
      doTest(new ResteasyAuthenticationException(), new AuthenticationException());
   }

   @Test
   public void testCircularRedirectException() throws Exception
   {
      doTest(new ResteasyCircularRedirectException(), new CircularRedirectException());
   }


   @Test
   public void testClientProtocolException() throws Exception
   {
      doTest(new ResteasyClientProtocolException(), new ClientProtocolException());
   }

   @Test
   public void testConnectionClosedException() throws Exception
   {
      doTest(new ResteasyConnectionClosedException(), new ConnectionClosedException(""));
   }

   @Test
   public void testConnectionPoolTimeoutException() throws Exception
   {
      doTest(new ResteasyConnectionPoolTimeoutException(), new ConnectionPoolTimeoutException(""));
   }

   @Test
   public void testConnectTimeoutException() throws Exception
   {
      doTest(new ResteasyConnectTimeoutException(), new ConnectTimeoutException(""));
   }
   
   @Test
   public void testCookieRestrictionViolationException() throws Exception
   {
      doTest(new ResteasyCookieRestrictionViolationException(), new CookieRestrictionViolationException());
   }

   @Test
   public void testHttpException() throws Exception
   {
      doTest(new ResteasyHttpException(), new HttpException());
   }

   @Test
   public void testHttpHostConnectException() throws Exception
   {
      doTest(new ResteasyHttpHostConnectException(), new HttpHostConnectException(null, null));
   }
   
   @Test
   public void testInvalidCredentialsException() throws Exception
   {
      doTest(new ResteasyInvalidCredentialsException(), new InvalidCredentialsException());
   }

   @Test
   public void testIOException() throws Exception
   {
      doTest(new ResteasyIOException(), new IOException());
   }

   @Test
   public void testMalformedChallengeException() throws Exception
   {
      doTest(new ResteasyMalformedChallengeException(), new MalformedChallengeException());
   }

   @Test
   public void testMalformedChunkCodingException() throws Exception
   {
      doTest(new ResteasyMalformedChunkCodingException(), new MalformedChunkCodingException());
   }

   @Test
   public void testMalformedCookieException() throws Exception
   {
      doTest(new ResteasyMalformedCookieException(), new MalformedCookieException());
   }

   @Test
   public void testMethodNotSupportedException() throws Exception
   {
      doTest(new ResteasyMethodNotSupportedException(), new MethodNotSupportedException(""));
   }

   @Test
   public void testNoHttpResponseException() throws Exception
   {
      doTest(new ResteasyNoHttpResponseException(), new NoHttpResponseException(""));
   }

   @Test
   public void testNonRepeatableRequestException() throws Exception
   {
      doTest(new ResteasyNonRepeatableRequestException(), new NonRepeatableRequestException());
   }

   @Test
   public void testNTLMEngineException() throws Exception
   {
      doTest(new ResteasyNTLMEngineException(), new NTLMEngineException());
   }

   @Test
   public void testProtocolException() throws Exception
   {
      doTest(new ResteasyProtocolException(), new ProtocolException());
   }

   @Test
   public void testRedirectException() throws Exception
   {
      doTest(new ResteasyRedirectException(), new RedirectException());
   }

   @Test
   public void testTunnelRefusedException() throws Exception
   {
      doTest(new ResteasyTunnelRefusedException(), new TunnelRefusedException("", null));
   }

   @Test
   public void testUnsupportedHttpVersionException() throws Exception
   {
      doTest(new ResteasyUnsupportedHttpVersionException(), new UnsupportedHttpVersionException());
   }
   
   private void doTest(Exception resteasyException, Exception embeddedException) throws Exception
   {
      before();
      try
      {
         ProxyBuilder<Foo> builder = ProxyBuilder.build(Foo.class, "http://localhost:9999").serverMediaType(MediaType.TEXT_PLAIN_TYPE);
         builder.executor(new TestClientExecutor(embeddedException));
         Foo foo = builder.now();
         foo.getFoo();
      }
      catch (Throwable t)
      {
         traverseException(t);
         assertTrue("Expected instance of " + resteasyException.getClass() + ", got " + t, resteasyException.getClass().equals(t.getClass()));
         assertTrue("Expected instance of " + embeddedException.getClass() + ", got " + t, embeddedException.getClass().equals(t.getCause().getClass()));
      }
      finally
      {
         after();
      }
   }
     
   @Test
   public void testAlternativeMapperProviderInstance() throws Exception
   {
      beforeProviderInstance(new TestClientExceptionMapper());
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   @Test
   public void testAlternativeMapperClassName() throws Exception
   {
      beforeClassNames(TestClientExceptionMapper.class);
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   @Test
   public void testAlternativeMapperApplicationClasses() throws Exception
   {
      beforeApplicationClasses(TestClientExceptionMapper.class);
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   @Test
   public void testAlternativeMapperApplicationSingleton() throws Exception
   {
      beforeApplicationSingleton(new TestClientExceptionMapper());
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   @Test
   public void testAlternativeMapperContextParams() throws Exception
   {
      beforeContextParams(TestClientExceptionMapper.class);
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   @Test
   public void testAlternativeMapperInitParams() throws Exception
   {
      beforeInitParams(TestClientExceptionMapper.class);
      doTestWithAlternativeMapper(new TestException(null), new UnsupportedHttpVersionException());
   }
   
   private void doTestWithAlternativeMapper(Exception wrappingException, Exception embeddedException) throws Exception
   {
      try
      {
         ProxyBuilder<Foo> builder = ProxyBuilder.build(Foo.class, "http://localhost:9999").serverMediaType(MediaType.TEXT_PLAIN_TYPE);
         builder.executor(new TestClientExecutor(embeddedException));
         Foo foo = builder.now();
         foo.getFoo();
      }
      catch (Throwable t)
      {
         traverseException(t);
         assertTrue("Expected instance of " + wrappingException.getClass() + ", got " + t, wrappingException.getClass().equals(t.getClass()));
         assertTrue("Expected instance of " + embeddedException.getClass() + ", got " + t, embeddedException.getClass().equals(t.getCause().getClass()));
      }
      finally
      {
         after();
      }
   }

   static private void traverseException(Throwable t)
   {
      String indent = "";
      while (t instanceof Throwable)
      {
         System.out.println(indent + t);
         t = t.getCause();
         indent += "  ";
      }
   }
   
   static public class TestApplication extends Application
   {
      public Set<Class<?>> getClasses()
      {
         HashSet<Class<?>> classes = new HashSet<Class<?>>();
         classes.add(TestClientExceptionMapper.class);
         return classes;
      }
   }
   

   @Provider
   static class TestClientExecutor implements ClientExecutor
   {
      private Exception e;

      public TestClientExecutor(Exception e)
      {
         this.e = e;
         if (ResteasyProviderFactory.getInstance().getClientExceptionMapper(Exception.class) == null)
         {
            Type exceptionType = Types.getActualTypeArgumentsOfAnInterface(ApacheHttpClient4ExceptionMapper.class, ClientExceptionMapper.class)[0];
            ResteasyProviderFactory.getInstance().addClientExceptionMapper(new ApacheHttpClient4ExceptionMapper(), exceptionType);
         }
      }
      @Override
      public ClientRequest createRequest(String uriTemplate)
      {
         return null;
      }
      @Override
      public ClientRequest createRequest(UriBuilder uriBuilder)
      {
         return null;
      }
      @Override
      public ClientResponse<?> execute(ClientRequest request) throws Exception
      {
         throw e;
      }
      @Override
      public void close() throws Exception
      {
      }
   }
   
   static class TestException extends RuntimeException
   {
      private static final long serialVersionUID = -7825447948319726641L;
      
      public TestException(Exception e)
      {
         super(e);
      }
   }
   
   @Provider
   static public class TestClientExceptionMapper implements ClientExceptionMapper<Exception>
   {
      @Override
      public RuntimeException toException(Exception exception)
      {
         return new TestException(exception);
      }
      
   }
}
