/**
 * 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.
 */
package org.apache.hadoop.test.mock;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.hamcrest.Matcher;
import org.hamcrest.Matchers;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.xmlmatchers.XmlMatchers.isEquivalentTo;
import static org.xmlmatchers.transform.XmlConverters.the;
import static uk.co.datumedge.hamcrest.json.SameJSONAs.sameJSONAs;

public class MockRequestMatcher {

  private static final Charset UTF8 = Charset.forName( "UTF-8" );

  private String from;
  private MockResponseProvider response;
  private Set<String> methods = null;
  private String pathInfo = null;
  private String requestURL = null;
  Map<String,Matcher> headers = null;
  Set<Cookie> cookies = null;
  private Map<String,Object> attributes = null;
  private Map<String,String> queryParams = null;
  private String contentType = null;
  private String characterEncoding = null;
  private Integer contentLength = null;
  private byte[] entity = null;
  private Map<String,String[]> formParams = null;

  public MockRequestMatcher( MockResponseProvider response ) {
    this.response = response;
  }

  public MockResponseProvider respond() {
    return response;
  }

  public MockRequestMatcher from( String from ) {
    this.from = from;
    return this;
  }

  public MockRequestMatcher method( String... methods ) {
    if( this.methods == null ) {
      this.methods = new HashSet<String>();
    }
    if( methods != null ) {
      for( String method: methods ) {
        this.methods.add( method );
      }
    }
    return this;
  }

  public MockRequestMatcher pathInfo( String pathInfo ) {
    this.pathInfo = pathInfo;
    return this;
  }

  public MockRequestMatcher requestUrl( String requestUrl ) {
    this.requestURL = requestUrl;
    return this;
  }

  public MockRequestMatcher header( String name, String value ) {
    if( headers == null ) {
      headers = new HashMap<String, Matcher>();
    }
    headers.put( name, Matchers.is(value) );
    return this;
  }

  public MockRequestMatcher header( String name, Matcher matcher ) {
    if( headers == null ) {
      headers = new HashMap<String, Matcher>();
    }
    headers.put( name, matcher );
    return this;
  }

  public MockRequestMatcher cookie( Cookie cookie ) {
    if( cookies == null ) {
      cookies = new HashSet<Cookie>();
    }
    cookies.add( cookie );
    return this;
  }

  public MockRequestMatcher attribute( String name, Object value ) {
    if( this.attributes == null ) {
      this.attributes = new HashMap<String,Object>();
    }
    attributes.put( name, value );
    return this;
  }

  public MockRequestMatcher queryParam( String name, String value ) {
    if( this.queryParams == null ) {
      this.queryParams = new HashMap<String, String>();
    }
    queryParams.put( name, value );
    return this;
  }

  public MockRequestMatcher formParam( String name, String... values ) {
    if( entity != null ) {
      throw new IllegalStateException( "Entity already specified." );
    }
    if( formParams == null ) {
      formParams = new HashMap<String, String[]>();
    }
    String[] currentValues = formParams.get( name );
    if( currentValues == null ) {
      currentValues = values;
    } else if ( values != null ) {
      currentValues = ArrayUtils.addAll( currentValues, values );
    }
    formParams.put( name, currentValues );
    return this;
  }

  public MockRequestMatcher content( String string, Charset charset ) {
    content( string.getBytes( charset ) );
    return this;
  }

  public MockRequestMatcher content( byte[] entity ) {
    if( formParams != null ) {
      throw new IllegalStateException( "Form params already specified." );
    }
    this.entity = entity;
    return this;
  }

  public MockRequestMatcher content( URL url ) throws IOException {
    content( url.openStream() );
    return this;
  }

  public MockRequestMatcher content( InputStream stream ) throws IOException {
    content( IOUtils.toByteArray( stream ) );
    return this;
  }

  public MockRequestMatcher contentType( String contentType ) {
    this.contentType = contentType;
    return this;
  }

  public MockRequestMatcher contentLength( int length ) {
    this.contentLength = length;
    return this;
  }

  public MockRequestMatcher characterEncoding( String charset ) {
    this.characterEncoding = charset;
    return this;
  }

  public void match( HttpServletRequest request ) throws IOException {
    if( methods != null ) {
      assertThat(
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " is not using one of the expected HTTP methods",
          methods, hasItem( request.getMethod() ) );
    }
    if( pathInfo != null ) {
      assertThat(
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " does not have the expected pathInfo",
          request.getPathInfo(), is( pathInfo ) );
    }
    if( requestURL != null ) {
      assertThat( 
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " does not have the expected requestURL",
          request.getRequestURL().toString(), is( requestURL ) );
    }
    if( headers != null ) {
      for( String name: headers.keySet() ) {
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " does not have the expected value for header " + name,
            request.getHeader( name ),  headers.get(name) );
      }
    }
    if( cookies != null ) {
      List<Cookie> requestCookies = Arrays.asList( request.getCookies() );
      for( Cookie cookie: cookies ) {
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " does not have the expected cookie " + cookie,
            requestCookies, hasItem( cookie ) );
      }
    }
    if( contentType != null ) {
      String[] requestContentType = request.getContentType().split(";",2);
      assertThat(
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " does not have the expected content type",
          requestContentType[ 0 ], is( contentType ) );
    }
    if( characterEncoding != null ) {
      assertThat(
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " does not have the expected character encoding",
          request.getCharacterEncoding(), equalToIgnoringCase( characterEncoding ) );
    }
    if( contentLength != null ) {
      assertThat(
          "Request " + request.getMethod() + " " + request.getRequestURL() +
              " does not have the expected content length",
          request.getContentLength(), is( contentLength ) );
    }
    if( attributes != null ) {
      for( String name: attributes.keySet() ) {
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " is missing attribute '" + name + "'",
            request.getAttribute( name ), notNullValue() );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " has wrong value for attribute '" + name + "'",
            request.getAttribute( name ), is( request.getAttribute( name ) ) );
      }
    }
    // Note: Cannot use any of the expect.getParameter*() methods because they will read the
    // body and we don't want that to happen.
    if( queryParams != null ) {
      String queryString = request.getQueryString();
      Map<String,String[]> requestParams = parseQueryString( queryString == null ? "" : queryString );
      for( String name: queryParams.keySet() ) {
        String[] values = requestParams.get( name );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " query string " + queryString + " is missing parameter '" + name + "'",
            values, notNullValue() );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " query string " + queryString + " is missing a value for parameter '" + name + "'",
            Arrays.asList( values ), hasItem( queryParams.get( name ) ) );
      }
    }
    if( formParams != null ) {
      String paramString = IOUtils.toString( request.getInputStream(), request.getCharacterEncoding() );
      Map<String,String[]> requestParams = parseQueryString( paramString == null ? "" : paramString );
      for( String name: formParams.keySet() ) {
        String[] actualValues = requestParams.get( name );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " form params " + paramString + " is missing parameter '" + name + "'",
            actualValues, notNullValue() );
        String[] expectedValues = formParams.get( name );
        for( String expectedValue : expectedValues ) {
          assertThat(
              "Request " + request.getMethod() + " " + request.getRequestURL() +
                  " form params " + paramString + " is missing a value " + expectedValue + " for parameter '" + name + "'",
              Arrays.asList( actualValues ), hasItem( expectedValue ) );
        }
      }
    }
    if( entity != null ) {
      if( contentType != null && contentType.endsWith( "/xml" ) ) {
        String expectEncoding = characterEncoding;
        String expect = new String( entity, ( expectEncoding == null ? UTF8.name() : expectEncoding ) );
        String actualEncoding = request.getCharacterEncoding();
        String actual = IOUtils.toString( request.getInputStream(), actualEncoding == null ? UTF8.name() : actualEncoding );
        assertThat( the( actual ), isEquivalentTo( the( expect ) ) );
      } else if ( contentType != null && contentType.endsWith( "/json" ) )  {
        String expectEncoding = characterEncoding;
        String expect = new String( entity, ( expectEncoding == null ? UTF8.name() : expectEncoding ) );
        String actualEncoding = request.getCharacterEncoding();
        String actual = IOUtils.toString( request.getInputStream(), actualEncoding == null ? UTF8.name() : actualEncoding );
//        System.out.println( "EXPECT=" + expect );
//        System.out.println( "ACTUAL=" + actual );
        assertThat( actual, sameJSONAs( expect ) );
      } else if( characterEncoding == null || request.getCharacterEncoding() == null ) {
        byte[] bytes = IOUtils.toByteArray( request.getInputStream() );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " content does not match the expected content",
            bytes, is( entity ) );
      } else {
        String expect = new String( entity, characterEncoding );
        String actual = IOUtils.toString( request.getInputStream(), request.getCharacterEncoding() );
        assertThat(
            "Request " + request.getMethod() + " " + request.getRequestURL() +
                " content does not match the expected content",
            actual, is( expect ) );
      }
    }
  }

  public String toString() {
    return "from=" + from + ", pathInfo=" + pathInfo;
  }

  // Separate method to minimally scope the depreciation suppression.
  @SuppressWarnings("deprecation")
  private static Map<String,String[]> parseQueryString( String queryString ) {
    return javax.servlet.http.HttpUtils.parseQueryString( queryString );
  }

}
