/*
 *
 * Copyright 2018 gRPC authors.
 *
 * Licensed 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.
 *
 */

#import <XCTest/XCTest.h>

#include "src/core/lib/iomgr/port.h"

#ifdef GRPC_CFSTREAM

#include <netinet/in.h>

#include <grpc/grpc.h>
#include <grpc/impl/codegen/sync.h>
#include <grpc/support/sync.h>

#include "src/core/lib/address_utils/parse_address.h"
#include "src/core/lib/address_utils/sockaddr_utils.h"
#include "src/core/lib/event_engine/channel_args_endpoint_config.h"
#include "src/core/lib/iomgr/endpoint.h"
#include "src/core/lib/iomgr/resolve_address.h"
#include "src/core/lib/iomgr/tcp_client.h"
#include "src/core/lib/resource_quota/api.h"
#include "test/core/test_util/test_config.h"

static gpr_mu g_mu;
static int g_connections_complete = 0;
static grpc_endpoint* g_connecting = nullptr;

static void finish_connection() {
  gpr_mu_lock(&g_mu);
  g_connections_complete++;
  gpr_mu_unlock(&g_mu);
}

static void must_succeed(void* arg, grpc_error_handle error) {
  CHECK(g_connecting != nullptr);
  CHECK(error.ok());
  grpc_endpoint_destroy(g_connecting);
  g_connecting = nullptr;
  finish_connection();
}

static void must_fail(void* arg, grpc_error_handle error) {
  CHECK(g_connecting == nullptr);
  CHECK(!error.ok());
  NSLog(@"%s", grpc_core::StatusToString(error).c_str());
  finish_connection();
}

@interface CFStreamClientTests : XCTestCase

@end

@implementation CFStreamClientTests

+ (void)setUp {
  grpc_init();
  gpr_mu_init(&g_mu);
}

+ (void)tearDown {
  grpc_shutdown();
}

- (void)testSucceeds {
  int svr_fd;
  int r;
  int connections_complete_before;
  grpc_closure done;
  grpc_core::ExecCtx exec_ctx;

  VLOG(2) << "test_succeeds";

  auto resolved_addr = grpc_core::StringToSockaddr("127.0.0.1:0");
  CHECK(resolved_addr.ok());
  struct sockaddr_in* addr = reinterpret_cast<struct sockaddr_in*>(resolved_addr->addr);

  /* create a phony server */
  svr_fd = socket(AF_INET, SOCK_STREAM, 0);
  CHECK(svr_fd >= 0);
  CHECK(0 == bind(svr_fd, (struct sockaddr*)addr, (socklen_t)resolved_addr->len));
  CHECK(0 == listen(svr_fd, 1));

  gpr_mu_lock(&g_mu);
  connections_complete_before = g_connections_complete;
  gpr_mu_unlock(&g_mu);

  /* connect to it */
  CHECK(getsockname(svr_fd, (struct sockaddr*)addr, (socklen_t*)&resolved_addr->len) == 0);
  GRPC_CLOSURE_INIT(&done, must_succeed, nullptr, grpc_schedule_on_exec_ctx);
  auto args =
      grpc_core::CoreConfiguration::Get().channel_args_preconditioning().PreconditionChannelArgs(
          nullptr);
  grpc_tcp_client_connect(&done, &g_connecting, nullptr,
                          grpc_event_engine::experimental::ChannelArgsEndpointConfig(args),
                          &*resolved_addr, grpc_core::Timestamp::InfFuture());

  /* await the connection */
  do {
    resolved_addr->len = sizeof(addr);
    r = accept(svr_fd, reinterpret_cast<struct sockaddr*>(addr),
               reinterpret_cast<socklen_t*>(&resolved_addr->len));
  } while (r == -1 && errno == EINTR);
  CHECK(r >= 0);
  close(r);

  grpc_core::ExecCtx::Get()->Flush();

  /* wait for the connection callback to finish */
  gpr_mu_lock(&g_mu);
  NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:5];
  while (connections_complete_before == g_connections_complete) {
    gpr_mu_unlock(&g_mu);
    [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:deadline];
    gpr_mu_lock(&g_mu);
  }
  XCTAssertGreaterThan(g_connections_complete, connections_complete_before);

  gpr_mu_unlock(&g_mu);
}

- (void)testFails {
  grpc_core::ExecCtx exec_ctx;

  int connections_complete_before;
  grpc_closure done;
  int svr_fd;

  VLOG(2) << "test_fails";

  auto resolved_addr = grpc_core::StringToSockaddr("127.0.0.1:0");
  CHECK(resolved_addr.ok());
  struct sockaddr_in* addr = reinterpret_cast<struct sockaddr_in*>(resolved_addr->addr);

  svr_fd = socket(AF_INET, SOCK_STREAM, 0);
  CHECK(svr_fd >= 0);
  CHECK(0 == bind(svr_fd, (struct sockaddr*)addr, (socklen_t)resolved_addr->len));
  CHECK(0 == listen(svr_fd, 1));
  CHECK(getsockname(svr_fd, (struct sockaddr*)addr, (socklen_t*)&resolved_addr->len) == 0);
  close(svr_fd);

  gpr_mu_lock(&g_mu);
  connections_complete_before = g_connections_complete;
  gpr_mu_unlock(&g_mu);

  /* connect to a broken address */
  GRPC_CLOSURE_INIT(&done, must_fail, nullptr, grpc_schedule_on_exec_ctx);
  auto args =
      grpc_core::CoreConfiguration::Get().channel_args_preconditioning().PreconditionChannelArgs(
          nullptr);
  grpc_tcp_client_connect(&done, &g_connecting, nullptr,
                          grpc_event_engine::experimental::ChannelArgsEndpointConfig(args),
                          &*resolved_addr, grpc_core::Timestamp::InfFuture());

  grpc_core::ExecCtx::Get()->Flush();

  /* wait for the connection callback to finish */
  gpr_mu_lock(&g_mu);
  NSDate* deadline = [NSDate dateWithTimeIntervalSinceNow:5];
  while (g_connections_complete == connections_complete_before) {
    gpr_mu_unlock(&g_mu);
    [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:deadline];
    gpr_mu_lock(&g_mu);
  }

  XCTAssertGreaterThan(g_connections_complete, connections_complete_before);

  gpr_mu_unlock(&g_mu);
}

@end

#else  // GRPC_CFSTREAM

// Phony test suite
@interface CFStreamClientTests : XCTestCase

@end

@implementation CFStreamClientTests

- (void)setUp {
  [super setUp];
}

- (void)tearDown {
  [super tearDown];
}

@end

#endif  // GRPC_CFSTREAM
