/*
 * 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.servicecomb.common.rest.codec.param;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;

import org.apache.servicecomb.common.rest.RestConst;
import org.apache.servicecomb.common.rest.codec.RestClientRequest;
import org.apache.servicecomb.common.rest.codec.param.BodyProcessorCreator.BodyProcessor;
import org.apache.servicecomb.common.rest.codec.param.BodyProcessorCreator.RawJsonBodyProcessor;
import org.apache.servicecomb.foundation.vertx.stream.BufferInputStream;
import org.junit.jupiter.api.Assertions;
import org.junit.Before;
import org.junit.Test;

import com.fasterxml.jackson.databind.type.TypeFactory;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.impl.headers.HeadersMultiMap;
import mockit.Expectations;
import mockit.Mock;
import mockit.MockUp;
import mockit.Mocked;

public class TestBodyProcessor {
  @Mocked
  HttpServletRequest request;

  MultiMap headers;

  RestClientRequest clientRequest;

  ParamValueProcessor processor;

  ByteBuf inputBodyByteBuf = Unpooled.buffer();

  BufferInputStream inputStream = new BufferInputStream(inputBodyByteBuf);

  Buffer outputBodyBuffer;

  private void createProcessor(Class<?> type) {
    processor = new BodyProcessor(TypeFactory.defaultInstance().constructType(type), type.equals(String.class), true);
  }

  private void createRawJsonProcessor() {
    processor = new RawJsonBodyProcessor(TypeFactory.defaultInstance().constructType(String.class), true, true);
  }

  private void createClientRequest() {
    clientRequest = new MockUp<RestClientRequest>() {
      @Mock
      void putHeader(String name, String value) {
        headers.add(name, value);
      }

      @Mock
      void write(Buffer bodyBuffer) {
        outputBodyBuffer = bodyBuffer;
      }

      @Mock
      MultiMap getHeaders() {
        return headers;
      }
    }.getMockInstance();
  }

  private void initInputStream() throws IOException {
    new Expectations() {
      {
        request.getInputStream();
        result = inputStream;
      }
    };
  }

  private void setupGetValue(Class<?> type) throws IOException {
    createProcessor(type);
    initInputStream();
  }

  @Before
  public void before() {
    headers = new HeadersMultiMap();
  }

  @Test
  public void testGetValueHaveAttr() throws Exception {
    int body = 10;
    createProcessor(String.class);
    new Expectations() {
      {
        request.getAttribute(RestConst.BODY_PARAMETER);
        result = body;
      }
    };

    Object result = processor.getValue(request);
    Assertions.assertEquals("10", result);
  }

  @Test
  public void testGetValueNoAttrNoStream() throws Exception {
    createProcessor(String.class);
    new Expectations() {
      {
        request.getInputStream();
        result = null;
      }
    };

    Object result = processor.getValue(request);
    Assertions.assertNull(result);
  }

  @Test
  public void testGetValueTextPlain() throws Exception {
    setupGetValue(String.class);
    inputBodyByteBuf.writeCharSequence("abc", StandardCharsets.UTF_8);

    new Expectations() {
      {
        request.getContentType();
        result = MediaType.TEXT_PLAIN;
      }
    };

    Assertions.assertEquals("abc", processor.getValue(request));
  }

  @Test
  public void testGetValueContextTypeJson() throws Exception {
    setupGetValue(Integer.class);
    inputBodyByteBuf.writeCharSequence("\"1\"", StandardCharsets.UTF_8);

    new Expectations() {
      {
        request.getContentType();
        result = MediaType.APPLICATION_JSON;
      }
    };

    Assertions.assertEquals(1, processor.getValue(request));
  }

  @Test
  public void testGetValueDefaultJson() throws Exception {
    setupGetValue(Integer.class);
    inputBodyByteBuf.writeCharSequence("\"1\"", StandardCharsets.UTF_8);

    Assertions.assertEquals(1, processor.getValue(request));
  }

  @Test
  public void testSetValue() throws Exception {
    createClientRequest();
    createProcessor(String.class);

    processor.setValue(clientRequest, "value");
    Assertions.assertEquals(MediaType.APPLICATION_JSON, headers.get(HttpHeaders.CONTENT_TYPE));
    Assertions.assertEquals("\"value\"", outputBodyBuffer.toString());
  }

  @Test
  public void testSetValueTextPlain() throws Exception {
    createClientRequest();
    createProcessor(String.class);
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN);

    processor.setValue(clientRequest, "value");
    Assertions.assertEquals(MediaType.TEXT_PLAIN, headers.get(HttpHeaders.CONTENT_TYPE));
    Assertions.assertEquals("value", outputBodyBuffer.toString());
  }

  @Test
  public void testSetValueTextPlainTypeMismatch() {
    createClientRequest();
    createProcessor(String.class);
    headers.add(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN);

    try {
      processor.setValue(clientRequest, new Date());
      Assertions.fail("an exception is expected!");
    } catch (Exception e) {
      Assertions.assertEquals(IllegalArgumentException.class, e.getClass());
      Assertions.assertEquals("Content-Type is text/plain while arg type is not String", e.getMessage());
    }
  }

  @Test
  public void testGetParameterPath() {
    createProcessor(String.class);
    Assertions.assertEquals("", processor.getParameterPath());
  }

  @Test
  public void testGetProcessorType() {
    createProcessor(String.class);
    Assertions.assertEquals("body", processor.getProcessorType());
  }

  @Test
  public void testGetValueRawJson() throws Exception {
    createRawJsonProcessor();
    initInputStream();
    inputBodyByteBuf.writeCharSequence("\"1\"", StandardCharsets.UTF_8);

    Assertions.assertEquals("\"1\"", processor.getValue(request));
  }

  @Test
  public void testGetValueRawJsonHaveAttr() throws Exception {
    int body = 10;
    createRawJsonProcessor();
    new Expectations() {
      {
        request.getAttribute(RestConst.BODY_PARAMETER);
        result = body;
      }
    };

    Object result = processor.getValue(request);
    Assertions.assertEquals("10", result);
  }

  @Test
  public void testGetValueRawJsonNoAttrNoStream() throws Exception {
    createRawJsonProcessor();
    new Expectations() {
      {
        request.getInputStream();
        result = null;
      }
    };

    Object result = processor.getValue(request);
    Assertions.assertNull(result);
  }

  @Test
  public void testSetValueRawJson() throws Exception {
    createClientRequest();
    createRawJsonProcessor();

    processor.setValue(clientRequest, "value");
    Assertions.assertEquals(MediaType.APPLICATION_JSON, headers.get(HttpHeaders.CONTENT_TYPE));
    Assertions.assertEquals("value", outputBodyBuffer.toString());
  }

  static class BodyModel {
    public String name;

    public int age;
  }

  @Test
  public void convertFromFormData() throws Exception {
    createProcessor(BodyModel.class);
    Map<String, String[]> parameterMap = new HashMap<>();
    parameterMap.put("name", new String[] {"n"});
    parameterMap.put("age", new String[] {"10"});
    new Expectations() {
      {
        request.getParameterMap();
        result = parameterMap;
        request.getContentType();
        result = MediaType.MULTIPART_FORM_DATA + ";utf-8";
      }
    };

    BodyModel bm = (BodyModel) processor.getValue(request);
    Assertions.assertEquals("n", bm.name);
    Assertions.assertEquals(10, bm.age);
  }

  @Test
  public void convertFromUrlencoded() throws Exception {
    createProcessor(BodyModel.class);
    Map<String, String[]> parameterMap = new HashMap<>();
    parameterMap.put("name", new String[] {"n"});
    parameterMap.put("age", new String[] {"10"});
    new Expectations() {
      {
        request.getParameterMap();
        result = parameterMap;
        request.getContentType();
        result = MediaType.APPLICATION_FORM_URLENCODED + ";utf-8";
      }
    };

    BodyModel bm = (BodyModel) processor.getValue(request);
    Assertions.assertEquals("n", bm.name);
    Assertions.assertEquals(10, bm.age);
  }
}
