/*
 * 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.transport.highway;

import javax.ws.rs.core.Response.Status;
import javax.xml.ws.Holder;

import org.apache.servicecomb.codec.protobuf.definition.OperationProtobuf;
import org.apache.servicecomb.codec.protobuf.definition.ProtobufManager;
import org.apache.servicecomb.core.Const;
import org.apache.servicecomb.core.Endpoint;
import org.apache.servicecomb.core.Invocation;
import org.apache.servicecomb.core.definition.OperationConfig;
import org.apache.servicecomb.core.definition.OperationMeta;
import org.apache.servicecomb.core.executor.ReactiveExecutor;
import org.apache.servicecomb.core.invocation.InvocationStageTrace;
import org.apache.servicecomb.foundation.test.scaffolding.config.ArchaiusUtils;
import org.apache.servicecomb.foundation.vertx.VertxUtils;
import org.apache.servicecomb.foundation.vertx.client.ClientPoolManager;
import org.apache.servicecomb.foundation.vertx.client.tcp.AbstractTcpClientPackage;
import org.apache.servicecomb.foundation.vertx.client.tcp.NetClientWrapper;
import org.apache.servicecomb.foundation.vertx.client.tcp.TcpClientConfig;
import org.apache.servicecomb.foundation.vertx.client.tcp.TcpData;
import org.apache.servicecomb.foundation.vertx.client.tcp.TcpResponseCallback;
import org.apache.servicecomb.foundation.vertx.server.TcpParser;
import org.apache.servicecomb.foundation.vertx.tcp.TcpOutputStream;
import org.apache.servicecomb.swagger.invocation.Response;
import org.apache.servicecomb.swagger.invocation.exception.InvocationException;
import org.apache.servicecomb.transport.highway.message.LoginRequest;
import org.apache.servicecomb.transport.highway.message.RequestHeader;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;

import io.netty.buffer.ByteBuf;
import io.protostuff.runtime.ProtobufCompatibleUtils;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import mockit.Deencapsulation;
import mockit.Mock;
import mockit.MockUp;
import mockit.Mocked;

public class TestHighwayClient {
  private static final String REQUEST_TIMEOUT_KEY = "servicecomb.request.timeout";

  HighwayClient client = new HighwayClient();

  Invocation invocation = Mockito.mock(Invocation.class);

  InvocationStageTrace invocationStageTrace = new InvocationStageTrace(invocation);

  OperationProtobuf operationProtobuf = Mockito.mock(OperationProtobuf.class);

  OperationMeta operationMeta = Mockito.mock(OperationMeta.class);

  Endpoint endpoint = Mockito.mock(Endpoint.class);

  static long nanoTime = 123;

  @BeforeClass
  public static void setup() {
    ArchaiusUtils.resetConfig();
    ArchaiusUtils.setProperty(REQUEST_TIMEOUT_KEY, 2000);

    new MockUp<System>() {
      @Mock
      long nanoTime() {
        return nanoTime;
      }
    };
  }

  @AfterClass
  public static void teardown() {
    ArchaiusUtils.resetConfig();
  }

  @Test
  public void testLoginTimeout(@Mocked Vertx vertx) {
    TcpClientConfig tcpClientConfig = Deencapsulation.invoke(client, "createTcpClientConfig");
    Assert.assertEquals(2000, tcpClientConfig.getMsLoginTimeout());
  }

  @Test
  public void testHighwayClientSSL(@Mocked Vertx vertx) throws Exception {
    new MockUp<VertxUtils>() {
      @Mock
      <VERTICLE extends AbstractVerticle> boolean blockDeploy(Vertx vertx,
          Class<VERTICLE> cls,
          DeploymentOptions options) {
        return true;
      }
    };

    client.init(vertx);

    ClientPoolManager<HighwayClientConnectionPool> clientMgr = Deencapsulation.getField(client, "clientMgr");
    Assert.assertSame(vertx, Deencapsulation.getField(clientMgr, "vertx"));
  }

  private Object doTestSend(Vertx vertx, HighwayClientConnectionPool pool, HighwayClientConnection tcpClient,
      Object decodedResponse) throws Exception {
    new MockUp<VertxUtils>() {
      @Mock
      <VERTICLE extends AbstractVerticle> boolean blockDeploy(Vertx vertx,
          Class<VERTICLE> cls,
          DeploymentOptions options) {
        return true;
      }
    };

    new MockUp<ClientPoolManager<HighwayClientConnectionPool>>() {
      @Mock
      public HighwayClientConnectionPool findClientPool(boolean sync) {
        return pool;
      }
    };

    new MockUp<ProtobufManager>() {
      @Mock
      public OperationProtobuf getOrCreateOperation(OperationMeta operationMeta) {
        return operationProtobuf;
      }
    };

    new MockUp<HighwayClientConnectionPool>() {
      @Mock
      HighwayClientConnection findOrCreateClient(String endpoint) {
        return tcpClient;
      }
    };

    new MockUp<HighwayCodec>() {
      @Mock
      public Buffer encodeRequest(Invocation invocation, OperationProtobuf operationProtobuf,
          long msgId) {
        return null;
      }

      @Mock
      Response decodeResponse(Invocation invocation, OperationProtobuf operationProtobuf, TcpData tcpData)
          throws Throwable {
        if (decodedResponse instanceof Response) {
          return (Response) decodedResponse;
        }

        throw (Throwable) decodedResponse;
      }
    };

    client.init(vertx);

    Mockito.when(invocation.getOperationMeta()).thenReturn(operationMeta);
    Mockito.when(invocation.getEndpoint()).thenReturn(endpoint);
    Mockito.when(invocation.getEndpoint().getEndpoint()).thenReturn("endpoint");
    Mockito.when(invocation.getResponseExecutor()).thenReturn(new ReactiveExecutor());
    Mockito.when(invocation.getInvocationStageTrace()).thenReturn(invocationStageTrace);
    Mockito.when(operationMeta.getConfig()).thenReturn(Mockito.mock(OperationConfig.class));

    Holder<Object> result = new Holder<>();
    client.send(invocation, ar -> {
      result.value = ar.getResult();
    });

    return result.value;
  }

  @Test
  public void testSend_success(@Mocked Vertx vertx, @Mocked HighwayClientConnectionPool pool,
      @Mocked HighwayClientConnection tcpClient) throws Exception {
    new MockUp<HighwayClientConnection>() {
      @Mock
      void send(AbstractTcpClientPackage tcpClientPackage, TcpResponseCallback callback) {
        callback.success(null);
      }
    };
    new MockUp<HighwayClientPackage>() {
      @Mock
      public long getFinishWriteToBuffer() {
        return nanoTime;
      }
    };
    Object result = doTestSend(vertx, pool, tcpClient, Response.ok("ok"));

    Assert.assertEquals("ok", result);
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersRequest());
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersResponse());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishClientFiltersResponse());

    Assert.assertEquals(nanoTime, invocationStageTrace.getStartSend());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishGetConnection());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishWriteToBuffer());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishReceiveResponse());
  }

  @Test
  public void testSend_success_decode_failed(@Mocked Vertx vertx, @Mocked HighwayClientConnectionPool pool,
      @Mocked HighwayClientConnection tcpClient) throws Exception {
    new MockUp<HighwayClientConnection>() {
      @Mock
      void send(AbstractTcpClientPackage tcpClientPackage, TcpResponseCallback callback) {
        callback.success(null);
      }
    };

    Object result = doTestSend(vertx, pool, tcpClient, new InvocationException(Status.BAD_REQUEST, (Object) "failed"));

    Assert.assertEquals("failed", ((InvocationException) result).getErrorData());
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersRequest());
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersResponse());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishClientFiltersResponse());
  }

  @Test
  public void testSend_failed(@Mocked Vertx vertx, @Mocked HighwayClientConnectionPool pool,
      @Mocked HighwayClientConnection tcpClient) throws Exception {
    new MockUp<HighwayClientConnection>() {
      @Mock
      void send(AbstractTcpClientPackage tcpClientPackage, TcpResponseCallback callback) {
        callback.fail(new InvocationException(Status.BAD_REQUEST, (Object) "failed"));
      }
    };

    Object result = doTestSend(vertx,
        pool,
        tcpClient,
        null);

    Assert.assertEquals("failed", ((InvocationException) result).getErrorData());
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersRequest());
    Assert.assertEquals(nanoTime, invocationStageTrace.getStartClientFiltersResponse());
    Assert.assertEquals(nanoTime, invocationStageTrace.getFinishClientFiltersResponse());
  }

  @Test
  public void testCreateLogin(@Mocked NetClientWrapper netClientWrapper) throws Exception {
    ProtobufCompatibleUtils.init();

    HighwayClientConnection connection =
        new HighwayClientConnection(null, netClientWrapper, "highway://127.0.0.1:7890");
    TcpOutputStream os = connection.createLogin();
    ByteBuf buf = os.getBuffer().getByteBuf();

    byte[] magic = new byte[TcpParser.TCP_MAGIC.length];
    buf.readBytes(magic);
    Assert.assertArrayEquals(TcpParser.TCP_MAGIC, magic);
    Assert.assertEquals(os.getMsgId(), buf.readLong());

    int start = TcpParser.TCP_HEADER_LENGTH;
    int totalLen = buf.readInt();
    int headerLen = buf.readInt();
    Buffer headerBuffer =
        os.getBuffer().slice(start, start + headerLen);
    int end = start + totalLen;
    start += headerLen;
    Buffer bodyBuffer = os.getBuffer().slice(start, end);

    RequestHeader header = RequestHeader.readObject(headerBuffer);
    Assert.assertEquals(MsgType.LOGIN, header.getMsgType());

    LoginRequest login = LoginRequest.readObject(bodyBuffer);
    Assert.assertEquals(Const.HIGHWAY, login.getProtocol());
  }
}
