/*
 *
 * Copyright 2016 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.
 *
 */

#include <string.h>

#include <grpc/grpc.h>
#include <grpc/grpc_security.h>
#include <grpc/support/alloc.h>
#include <grpc/support/log.h>
#include <grpc/support/string_util.h>

#include "src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_wrapper.h"
#include "src/core/ext/filters/client_channel/server_address.h"
#include "src/core/ext/transport/chttp2/transport/chttp2_transport.h"
#include "src/core/lib/channel/channel_args.h"
#include "src/core/lib/gpr/env.h"
#include "src/core/lib/iomgr/executor.h"
#include "src/core/lib/iomgr/resolve_address.h"
#include "src/core/lib/iomgr/tcp_client.h"
#include "src/core/lib/iomgr/timer.h"
#include "src/core/lib/iomgr/timer_manager.h"
#include "src/core/lib/slice/slice_internal.h"
#include "src/core/lib/surface/server.h"
#include "src/core/lib/transport/metadata.h"
#include "test/core/end2end/data/ssl_test_data.h"
#include "test/core/util/fuzzer_util.h"
#include "test/core/util/passthru_endpoint.h"

using grpc_core::testing::grpc_fuzzer_get_next_byte;
using grpc_core::testing::grpc_fuzzer_get_next_string;
using grpc_core::testing::grpc_fuzzer_get_next_uint32;
using grpc_core::testing::input_stream;

////////////////////////////////////////////////////////////////////////////////
// logging

bool squelch = true;
bool leak_check = true;

static void dont_log(gpr_log_func_args* /*args*/) {}

////////////////////////////////////////////////////////////////////////////////
// global state

static gpr_timespec g_now;
static grpc_server* g_server;
static grpc_channel* g_channel;
static grpc_resource_quota* g_resource_quota;

extern gpr_timespec (*gpr_now_impl)(gpr_clock_type clock_type);

static gpr_timespec now_impl(gpr_clock_type clock_type) {
  GPR_ASSERT(clock_type != GPR_TIMESPAN);
  gpr_timespec ts = g_now;
  ts.clock_type = clock_type;
  return ts;
}

static void end(input_stream* inp) { inp->cur = inp->end; }

static void read_buffer(input_stream* inp, char** buffer, size_t* length,
                        bool* special) {
  *length = grpc_fuzzer_get_next_byte(inp);
  if (*length == 255) {
    if (special != nullptr) *special = true;
    *length = grpc_fuzzer_get_next_byte(inp);
  } else {
    if (special != nullptr) *special = false;
  }
  *buffer = static_cast<char*>(gpr_malloc(*length));
  for (size_t i = 0; i < *length; i++) {
    (*buffer)[i] = static_cast<char>(grpc_fuzzer_get_next_byte(inp));
  }
}

static grpc_slice maybe_intern(grpc_slice s, bool intern) {
  grpc_slice r = intern ? grpc_slice_intern(s) : grpc_slice_ref(s);
  grpc_slice_unref(s);
  return r;
}

static grpc_slice read_string_like_slice(input_stream* inp) {
  bool special;
  char* s = grpc_fuzzer_get_next_string(inp, &special);
  grpc_slice r = maybe_intern(grpc_slice_from_copied_string(s), special);
  gpr_free(s);
  return r;
}

static grpc_slice read_buffer_like_slice(input_stream* inp) {
  char* buffer;
  size_t length;
  bool special;
  read_buffer(inp, &buffer, &length, &special);
  grpc_slice r =
      maybe_intern(grpc_slice_from_copied_buffer(buffer, length), special);
  gpr_free(buffer);
  return r;
}

static uint32_t read_uint22(input_stream* inp) {
  uint8_t b = grpc_fuzzer_get_next_byte(inp);
  uint32_t x = b & 0x7f;
  if (b & 0x80) {
    x <<= 7;
    b = grpc_fuzzer_get_next_byte(inp);
    x |= b & 0x7f;
    if (b & 0x80) {
      x <<= 8;
      x |= grpc_fuzzer_get_next_byte(inp);
    }
  }
  return x;
}

static grpc_byte_buffer* read_message(input_stream* inp) {
  grpc_slice slice = grpc_slice_malloc(read_uint22(inp));
  memset(GRPC_SLICE_START_PTR(slice), 0, GRPC_SLICE_LENGTH(slice));
  grpc_byte_buffer* out = grpc_raw_byte_buffer_create(&slice, 1);
  grpc_slice_unref(slice);
  return out;
}

static int read_int(input_stream* inp) {
  return static_cast<int>(grpc_fuzzer_get_next_uint32(inp));
}

static grpc_channel_args* read_args(input_stream* inp) {
  size_t n = grpc_fuzzer_get_next_byte(inp);
  grpc_arg* args = static_cast<grpc_arg*>(gpr_malloc(sizeof(*args) * n));
  for (size_t i = 0; i < n; i++) {
    switch (grpc_fuzzer_get_next_byte(inp)) {
      case 1:
        args[i].type = GRPC_ARG_STRING;
        args[i].key = grpc_fuzzer_get_next_string(inp, nullptr);
        args[i].value.string = grpc_fuzzer_get_next_string(inp, nullptr);
        break;
      case 2:
        args[i].type = GRPC_ARG_INTEGER;
        args[i].key = grpc_fuzzer_get_next_string(inp, nullptr);
        args[i].value.integer = read_int(inp);
        break;
      case 3:
        args[i].type = GRPC_ARG_POINTER;
        args[i].key = gpr_strdup(GRPC_ARG_RESOURCE_QUOTA);
        args[i].value.pointer.vtable = grpc_resource_quota_arg_vtable();
        args[i].value.pointer.p = g_resource_quota;
        grpc_resource_quota_ref(g_resource_quota);
        break;
      default:
        end(inp);
        n = i;
        break;
    }
  }
  grpc_channel_args* a =
      static_cast<grpc_channel_args*>(gpr_malloc(sizeof(*a)));
  a->args = args;
  a->num_args = n;
  return a;
}

typedef struct cred_artifact_ctx {
  int num_release;
  char* release[3];
} cred_artifact_ctx;
#define CRED_ARTIFACT_CTX_INIT \
  {                            \
    0, { 0 }                   \
  }

static void cred_artifact_ctx_finish(cred_artifact_ctx* ctx) {
  for (int i = 0; i < ctx->num_release; i++) {
    gpr_free(ctx->release[i]);
  }
}

static const char* read_cred_artifact(cred_artifact_ctx* ctx, input_stream* inp,
                                      const char** builtins,
                                      size_t num_builtins) {
  uint8_t b = grpc_fuzzer_get_next_byte(inp);
  if (b == 0) return nullptr;
  if (b == 1)
    return ctx->release[ctx->num_release++] =
               grpc_fuzzer_get_next_string(inp, nullptr);
  if (b >= num_builtins + 1) {
    end(inp);
    return nullptr;
  }
  return builtins[b - 1];
}

static grpc_channel_credentials* read_ssl_channel_creds(input_stream* inp) {
  cred_artifact_ctx ctx = CRED_ARTIFACT_CTX_INIT;
  static const char* builtin_root_certs[] = {test_root_cert};
  static const char* builtin_private_keys[] = {
      test_server1_key, test_self_signed_client_key, test_signed_client_key};
  static const char* builtin_cert_chains[] = {
      test_server1_cert, test_self_signed_client_cert, test_signed_client_cert};
  const char* root_certs = read_cred_artifact(
      &ctx, inp, builtin_root_certs, GPR_ARRAY_SIZE(builtin_root_certs));
  const char* private_key = read_cred_artifact(
      &ctx, inp, builtin_private_keys, GPR_ARRAY_SIZE(builtin_private_keys));
  const char* certs = read_cred_artifact(&ctx, inp, builtin_cert_chains,
                                         GPR_ARRAY_SIZE(builtin_cert_chains));
  grpc_ssl_pem_key_cert_pair key_cert_pair = {private_key, certs};
  grpc_channel_credentials* creds = grpc_ssl_credentials_create(
      root_certs,
      private_key != nullptr && certs != nullptr ? &key_cert_pair : nullptr,
      nullptr, nullptr);
  cred_artifact_ctx_finish(&ctx);
  return creds;
}

static grpc_call_credentials* read_call_creds(input_stream* inp, int depth) {
  if (depth > 64) {
    // prevent creating infinitely deep call creds
    end(inp);
    return nullptr;
  }
  switch (grpc_fuzzer_get_next_byte(inp)) {
    default:
      end(inp);
      return nullptr;
    case 0:
      return nullptr;
    case 1: {
      grpc_call_credentials* c1 = read_call_creds(inp, depth + 1);
      grpc_call_credentials* c2 = read_call_creds(inp, depth + 1);
      if (c1 != nullptr && c2 != nullptr) {
        grpc_call_credentials* out =
            grpc_composite_call_credentials_create(c1, c2, nullptr);
        grpc_call_credentials_release(c1);
        grpc_call_credentials_release(c2);
        return out;
      } else if (c1 != nullptr) {
        return c1;
      } else if (c2 != nullptr) {
        return c2;
      } else {
        return nullptr;
      }
      GPR_UNREACHABLE_CODE(return nullptr);
    }
    case 2: {
      cred_artifact_ctx ctx = CRED_ARTIFACT_CTX_INIT;
      const char* access_token = read_cred_artifact(&ctx, inp, nullptr, 0);
      grpc_call_credentials* out =
          access_token == nullptr
              ? nullptr
              : grpc_access_token_credentials_create(access_token, nullptr);
      cred_artifact_ctx_finish(&ctx);
      return out;
    }
    case 3: {
      cred_artifact_ctx ctx = CRED_ARTIFACT_CTX_INIT;
      const char* auth_token = read_cred_artifact(&ctx, inp, nullptr, 0);
      const char* auth_selector = read_cred_artifact(&ctx, inp, nullptr, 0);
      grpc_call_credentials* out =
          auth_token == nullptr || auth_selector == nullptr
              ? nullptr
              : grpc_google_iam_credentials_create(auth_token, auth_selector,
                                                   nullptr);
      cred_artifact_ctx_finish(&ctx);
      return out;
    }
      /* TODO(ctiller): more cred types here */
  }
}

static grpc_channel_credentials* read_channel_creds(input_stream* inp) {
  switch (grpc_fuzzer_get_next_byte(inp)) {
    case 0:
      return read_ssl_channel_creds(inp);
      break;
    case 1: {
      grpc_channel_credentials* c1 = read_channel_creds(inp);
      grpc_call_credentials* c2 = read_call_creds(inp, 0);
      if (c1 != nullptr && c2 != nullptr) {
        grpc_channel_credentials* out =
            grpc_composite_channel_credentials_create(c1, c2, nullptr);
        grpc_channel_credentials_release(c1);
        grpc_call_credentials_release(c2);
        return out;
      } else if (c1) {
        return c1;
      } else if (c2) {
        grpc_call_credentials_release(c2);
        return nullptr;
      } else {
        return nullptr;
      }
      GPR_UNREACHABLE_CODE(return nullptr);
    }
    case 2:
      return nullptr;
    default:
      end(inp);
      return nullptr;
  }
}

static bool is_eof(input_stream* inp) { return inp->cur == inp->end; }

////////////////////////////////////////////////////////////////////////////////
// dns resolution

typedef struct addr_req {
  grpc_timer timer;
  char* addr;
  grpc_closure* on_done;
  grpc_resolved_addresses** addrs;
  std::unique_ptr<grpc_core::ServerAddressList>* addresses;
} addr_req;

static void finish_resolve(void* arg, grpc_error* error) {
  addr_req* r = static_cast<addr_req*>(arg);

  if (error == GRPC_ERROR_NONE && 0 == strcmp(r->addr, "server")) {
    if (r->addrs != nullptr) {
      grpc_resolved_addresses* addrs =
          static_cast<grpc_resolved_addresses*>(gpr_malloc(sizeof(*addrs)));
      addrs->naddrs = 1;
      addrs->addrs = static_cast<grpc_resolved_address*>(
          gpr_malloc(sizeof(*addrs->addrs)));
      addrs->addrs[0].len = 0;
      *r->addrs = addrs;
    } else if (r->addresses != nullptr) {
      *r->addresses = grpc_core::MakeUnique<grpc_core::ServerAddressList>();
      grpc_resolved_address dummy_resolved_address;
      memset(&dummy_resolved_address, 0, sizeof(dummy_resolved_address));
      dummy_resolved_address.len = 0;
      (*r->addresses)->emplace_back(dummy_resolved_address, nullptr);
    }
    grpc_core::ExecCtx::Run(DEBUG_LOCATION, r->on_done, GRPC_ERROR_NONE);
  } else {
    grpc_core::ExecCtx::Run(DEBUG_LOCATION, r->on_done,
                            GRPC_ERROR_CREATE_REFERENCING_FROM_STATIC_STRING(
                                "Resolution failed", &error, 1));
  }

  gpr_free(r->addr);
  delete r;
}

void my_resolve_address(const char* addr, const char* /*default_port*/,
                        grpc_pollset_set* /*interested_parties*/,
                        grpc_closure* on_done,
                        grpc_resolved_addresses** addrs) {
  addr_req* r = new addr_req();
  r->addr = gpr_strdup(addr);
  r->on_done = on_done;
  r->addrs = addrs;
  grpc_timer_init(
      &r->timer, GPR_MS_PER_SEC + grpc_core::ExecCtx::Get()->Now(),
      GRPC_CLOSURE_CREATE(finish_resolve, r, grpc_schedule_on_exec_ctx));
}

static grpc_address_resolver_vtable fuzzer_resolver = {my_resolve_address,
                                                       nullptr};

grpc_ares_request* my_dns_lookup_ares_locked(
    const char* /*dns_server*/, const char* addr, const char* /*default_port*/,
    grpc_pollset_set* /*interested_parties*/, grpc_closure* on_done,
    std::unique_ptr<grpc_core::ServerAddressList>* addresses,
    bool /*check_grpclb*/, char** /*service_config_json*/,
    int /*query_timeout*/, grpc_core::Combiner* /*combiner*/) {
  addr_req* r = static_cast<addr_req*>(gpr_malloc(sizeof(*r)));
  r->addr = gpr_strdup(addr);
  r->on_done = on_done;
  r->addrs = nullptr;
  r->addresses = addresses;
  grpc_timer_init(
      &r->timer, GPR_MS_PER_SEC + grpc_core::ExecCtx::Get()->Now(),
      GRPC_CLOSURE_CREATE(finish_resolve, r, grpc_schedule_on_exec_ctx));
  return nullptr;
}

static void my_cancel_ares_request_locked(grpc_ares_request* request) {
  GPR_ASSERT(request == nullptr);
}

////////////////////////////////////////////////////////////////////////////////
// client connection

static void sched_connect(grpc_closure* closure, grpc_endpoint** ep,
                          gpr_timespec deadline);

typedef struct {
  grpc_timer timer;
  grpc_closure* closure;
  grpc_endpoint** ep;
  gpr_timespec deadline;
} future_connect;

static void do_connect(void* arg, grpc_error* error) {
  future_connect* fc = static_cast<future_connect*>(arg);
  if (error != GRPC_ERROR_NONE) {
    *fc->ep = nullptr;
    grpc_core::ExecCtx::Run(DEBUG_LOCATION, fc->closure, GRPC_ERROR_REF(error));
  } else if (g_server != nullptr) {
    grpc_endpoint* client;
    grpc_endpoint* server;
    grpc_passthru_endpoint_create(&client, &server, g_resource_quota, nullptr);
    *fc->ep = client;

    grpc_transport* transport =
        grpc_create_chttp2_transport(nullptr, server, false);
    grpc_server_setup_transport(g_server, transport, nullptr, nullptr, nullptr);
    grpc_chttp2_transport_start_reading(transport, nullptr, nullptr);

    grpc_core::ExecCtx::Run(DEBUG_LOCATION, fc->closure, GRPC_ERROR_NONE);
  } else {
    sched_connect(fc->closure, fc->ep, fc->deadline);
  }
  gpr_free(fc);
}

static void sched_connect(grpc_closure* closure, grpc_endpoint** ep,
                          gpr_timespec deadline) {
  if (gpr_time_cmp(deadline, gpr_now(deadline.clock_type)) < 0) {
    *ep = nullptr;
    grpc_core::ExecCtx::Run(
        DEBUG_LOCATION, closure,
        GRPC_ERROR_CREATE_FROM_STATIC_STRING("Connect deadline exceeded"));
    return;
  }

  future_connect* fc = static_cast<future_connect*>(gpr_malloc(sizeof(*fc)));
  fc->closure = closure;
  fc->ep = ep;
  fc->deadline = deadline;
  grpc_timer_init(
      &fc->timer, GPR_MS_PER_SEC + grpc_core::ExecCtx::Get()->Now(),
      GRPC_CLOSURE_CREATE(do_connect, fc, grpc_schedule_on_exec_ctx));
}

static void my_tcp_client_connect(grpc_closure* closure, grpc_endpoint** ep,
                                  grpc_pollset_set* /*interested_parties*/,
                                  const grpc_channel_args* /*channel_args*/,
                                  const grpc_resolved_address* /*addr*/,
                                  grpc_millis deadline) {
  sched_connect(closure, ep,
                grpc_millis_to_timespec(deadline, GPR_CLOCK_MONOTONIC));
}

grpc_tcp_client_vtable fuzz_tcp_client_vtable = {my_tcp_client_connect};

////////////////////////////////////////////////////////////////////////////////
// test driver

typedef struct validator {
  void (*validate)(void* arg, bool success);
  void* arg;
} validator;

static validator* create_validator(void (*validate)(void* arg, bool success),
                                   void* arg) {
  validator* v = static_cast<validator*>(gpr_malloc(sizeof(*v)));
  v->validate = validate;
  v->arg = arg;
  return v;
}

static void assert_success_and_decrement(void* counter, bool success) {
  GPR_ASSERT(success);
  --*static_cast<int*>(counter);
}

static void decrement(void* counter, bool /*success*/) {
  --*static_cast<int*>(counter);
}

typedef struct connectivity_watch {
  int* counter;
  gpr_timespec deadline;
} connectivity_watch;

static connectivity_watch* make_connectivity_watch(gpr_timespec s,
                                                   int* counter) {
  connectivity_watch* o =
      static_cast<connectivity_watch*>(gpr_malloc(sizeof(*o)));
  o->deadline = s;
  o->counter = counter;
  return o;
}

static void validate_connectivity_watch(void* p, bool success) {
  connectivity_watch* w = static_cast<connectivity_watch*>(p);
  if (!success) {
    GPR_ASSERT(gpr_time_cmp(gpr_now(w->deadline.clock_type), w->deadline) >= 0);
  }
  --*w->counter;
  gpr_free(w);
}

static void free_non_null(void* p) {
  GPR_ASSERT(p != nullptr);
  gpr_free(p);
}

typedef enum { ROOT, CLIENT, SERVER, PENDING_SERVER } call_state_type;

#define DONE_FLAG_CALL_CLOSED ((uint64_t)(1 << 0))

typedef struct call_state {
  call_state_type type;
  grpc_call* call;
  grpc_byte_buffer* recv_message;
  grpc_status_code status;
  grpc_metadata_array recv_initial_metadata;
  grpc_metadata_array recv_trailing_metadata;
  grpc_slice recv_status_details;
  int cancelled;
  int pending_ops;
  bool sent_initial_metadata;
  grpc_call_details call_details;
  grpc_byte_buffer* send_message;
  // starts at 0, individual flags from DONE_FLAG_xxx are set
  // as different operations are completed
  uint64_t done_flags;

  // array of pointers to free later
  size_t num_to_free;
  size_t cap_to_free;
  void** to_free;

  // array of slices to unref
  size_t num_slices_to_unref;
  size_t cap_slices_to_unref;
  grpc_slice** slices_to_unref;

  struct call_state* next;
  struct call_state* prev;
} call_state;

static call_state* g_active_call;

static call_state* new_call(call_state* sibling, call_state_type type) {
  call_state* c = static_cast<call_state*>(gpr_malloc(sizeof(*c)));
  memset(c, 0, sizeof(*c));
  if (sibling != nullptr) {
    c->next = sibling;
    c->prev = sibling->prev;
    c->next->prev = c->prev->next = c;
  } else {
    c->next = c->prev = c;
  }
  c->type = type;
  return c;
}

static call_state* maybe_delete_call_state(call_state* call) {
  call_state* next = call->next;

  if (call->call != nullptr) return next;
  if (call->pending_ops != 0) return next;

  if (call == g_active_call) {
    g_active_call = call->next;
    GPR_ASSERT(call != g_active_call);
  }

  call->prev->next = call->next;
  call->next->prev = call->prev;
  grpc_metadata_array_destroy(&call->recv_initial_metadata);
  grpc_metadata_array_destroy(&call->recv_trailing_metadata);
  grpc_slice_unref(call->recv_status_details);
  grpc_call_details_destroy(&call->call_details);

  for (size_t i = 0; i < call->num_slices_to_unref; i++) {
    grpc_slice_unref(*call->slices_to_unref[i]);
    gpr_free(call->slices_to_unref[i]);
  }
  for (size_t i = 0; i < call->num_to_free; i++) {
    gpr_free(call->to_free[i]);
  }
  gpr_free(call->to_free);
  gpr_free(call->slices_to_unref);

  gpr_free(call);

  return next;
}

static void add_to_free(call_state* call, void* p) {
  if (call->num_to_free == call->cap_to_free) {
    call->cap_to_free = GPR_MAX(8, 2 * call->cap_to_free);
    call->to_free = static_cast<void**>(
        gpr_realloc(call->to_free, sizeof(*call->to_free) * call->cap_to_free));
  }
  call->to_free[call->num_to_free++] = p;
}

static grpc_slice* add_slice_to_unref(call_state* call, grpc_slice s) {
  if (call->num_slices_to_unref == call->cap_slices_to_unref) {
    call->cap_slices_to_unref = GPR_MAX(8, 2 * call->cap_slices_to_unref);
    call->slices_to_unref = static_cast<grpc_slice**>(gpr_realloc(
        call->slices_to_unref,
        sizeof(*call->slices_to_unref) * call->cap_slices_to_unref));
  }
  call->slices_to_unref[call->num_slices_to_unref] =
      static_cast<grpc_slice*>(gpr_malloc(sizeof(grpc_slice)));
  *call->slices_to_unref[call->num_slices_to_unref++] = s;
  return call->slices_to_unref[call->num_slices_to_unref - 1];
}

static void read_metadata(input_stream* inp, size_t* count,
                          grpc_metadata** metadata, call_state* cs) {
  *count = grpc_fuzzer_get_next_byte(inp);
  if (*count) {
    *metadata =
        static_cast<grpc_metadata*>(gpr_malloc(*count * sizeof(**metadata)));
    memset(*metadata, 0, *count * sizeof(**metadata));
    for (size_t i = 0; i < *count; i++) {
      (*metadata)[i].key = read_string_like_slice(inp);
      (*metadata)[i].value = read_buffer_like_slice(inp);
      (*metadata)[i].flags = grpc_fuzzer_get_next_uint32(inp);
      add_slice_to_unref(cs, (*metadata)[i].key);
      add_slice_to_unref(cs, (*metadata)[i].value);
    }
  } else {
    *metadata = static_cast<grpc_metadata*>(gpr_malloc(1));
  }
  add_to_free(cs, *metadata);
}

static call_state* destroy_call(call_state* call) {
  grpc_call_unref(call->call);
  call->call = nullptr;
  return maybe_delete_call_state(call);
}

static void finished_request_call(void* csp, bool success) {
  call_state* cs = static_cast<call_state*>(csp);
  GPR_ASSERT(cs->pending_ops > 0);
  --cs->pending_ops;
  if (success) {
    GPR_ASSERT(cs->call != nullptr);
    cs->type = SERVER;
  } else {
    maybe_delete_call_state(cs);
  }
}

typedef struct {
  call_state* cs;
  uint8_t has_ops;
} batch_info;

static void finished_batch(void* p, bool /*success*/) {
  batch_info* bi = static_cast<batch_info*>(p);
  --bi->cs->pending_ops;
  if ((bi->has_ops & (1u << GRPC_OP_RECV_MESSAGE)) &&
      (bi->cs->done_flags & DONE_FLAG_CALL_CLOSED)) {
    GPR_ASSERT(bi->cs->recv_message == nullptr);
  }
  if ((bi->has_ops & (1u << GRPC_OP_RECV_MESSAGE) &&
       bi->cs->recv_message != nullptr)) {
    grpc_byte_buffer_destroy(bi->cs->recv_message);
    bi->cs->recv_message = nullptr;
  }
  if ((bi->has_ops & (1u << GRPC_OP_SEND_MESSAGE))) {
    grpc_byte_buffer_destroy(bi->cs->send_message);
    bi->cs->send_message = nullptr;
  }
  if ((bi->has_ops & (1u << GRPC_OP_RECV_STATUS_ON_CLIENT)) ||
      (bi->has_ops & (1u << GRPC_OP_RECV_CLOSE_ON_SERVER))) {
    bi->cs->done_flags |= DONE_FLAG_CALL_CLOSED;
  }
  maybe_delete_call_state(bi->cs);
  gpr_free(bi);
}

static validator* make_finished_batch_validator(call_state* cs,
                                                uint8_t has_ops) {
  batch_info* bi = static_cast<batch_info*>(gpr_malloc(sizeof(*bi)));
  bi->cs = cs;
  bi->has_ops = has_ops;
  return create_validator(finished_batch, bi);
}

extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
  grpc_test_only_set_slice_hash_seed(0);
  char* grpc_trace_fuzzer = gpr_getenv("GRPC_TRACE_FUZZER");
  if (squelch && grpc_trace_fuzzer == nullptr) gpr_set_log_function(dont_log);
  gpr_free(grpc_trace_fuzzer);
  input_stream inp = {data, data + size};
  grpc_set_tcp_client_impl(&fuzz_tcp_client_vtable);
  gpr_now_impl = now_impl;
  grpc_init();
  grpc_timer_manager_set_threading(false);
  {
    grpc_core::ExecCtx exec_ctx;
    grpc_core::Executor::SetThreadingAll(false);
  }
  grpc_set_resolver_impl(&fuzzer_resolver);
  grpc_dns_lookup_ares_locked = my_dns_lookup_ares_locked;
  grpc_cancel_ares_request_locked = my_cancel_ares_request_locked;

  GPR_ASSERT(g_channel == nullptr);
  GPR_ASSERT(g_server == nullptr);

  bool server_shutdown = false;
  int pending_server_shutdowns = 0;
  int pending_channel_watches = 0;
  int pending_pings = 0;

  g_active_call = new_call(nullptr, ROOT);
  g_resource_quota = grpc_resource_quota_create("api_fuzzer");

  grpc_completion_queue* cq = grpc_completion_queue_create_for_next(nullptr);

  while (!is_eof(&inp) || g_channel != nullptr || g_server != nullptr ||
         pending_channel_watches > 0 || pending_pings > 0 ||
         g_active_call->type != ROOT || g_active_call->next != g_active_call) {
    if (is_eof(&inp)) {
      if (g_channel != nullptr) {
        grpc_channel_destroy(g_channel);
        g_channel = nullptr;
      }
      if (g_server != nullptr) {
        if (!server_shutdown) {
          grpc_server_shutdown_and_notify(
              g_server, cq,
              create_validator(assert_success_and_decrement,
                               &pending_server_shutdowns));
          server_shutdown = true;
          pending_server_shutdowns++;
        } else if (pending_server_shutdowns == 0) {
          grpc_server_destroy(g_server);
          g_server = nullptr;
        }
      }
      call_state* s = g_active_call;
      do {
        if (s->type != PENDING_SERVER && s->call != nullptr) {
          s = destroy_call(s);
        } else {
          s = s->next;
        }
      } while (s != g_active_call);

      g_now = gpr_time_add(g_now, gpr_time_from_seconds(1, GPR_TIMESPAN));
    }

    grpc_timer_manager_tick();

    switch (grpc_fuzzer_get_next_byte(&inp)) {
      // terminate on bad bytes
      default:
        end(&inp);
        break;
      // tickle completion queue
      case 0: {
        grpc_event ev = grpc_completion_queue_next(
            cq, gpr_inf_past(GPR_CLOCK_REALTIME), nullptr);
        switch (ev.type) {
          case GRPC_OP_COMPLETE: {
            validator* v = static_cast<validator*>(ev.tag);
            v->validate(v->arg, ev.success);
            gpr_free(v);
            break;
          }
          case GRPC_QUEUE_TIMEOUT:
            break;
          case GRPC_QUEUE_SHUTDOWN:
            abort();
            break;
        }
        break;
      }
      // increment global time
      case 1: {
        g_now = gpr_time_add(
            g_now, gpr_time_from_micros(grpc_fuzzer_get_next_uint32(&inp),
                                        GPR_TIMESPAN));
        break;
      }
      // create an insecure channel
      case 2: {
        if (g_channel == nullptr) {
          char* target = grpc_fuzzer_get_next_string(&inp, nullptr);
          char* target_uri;
          gpr_asprintf(&target_uri, "dns:%s", target);
          grpc_channel_args* args = read_args(&inp);
          g_channel = grpc_insecure_channel_create(target_uri, args, nullptr);
          GPR_ASSERT(g_channel != nullptr);
          {
            grpc_core::ExecCtx exec_ctx;
            grpc_channel_args_destroy(args);
          }
          gpr_free(target_uri);
          gpr_free(target);
        } else {
          end(&inp);
        }
        break;
      }
      // destroy a channel
      case 3: {
        if (g_channel != nullptr) {
          grpc_channel_destroy(g_channel);
          g_channel = nullptr;
        } else {
          end(&inp);
        }
        break;
      }
      // bring up a server
      case 4: {
        if (g_server == nullptr) {
          grpc_channel_args* args = read_args(&inp);
          g_server = grpc_server_create(args, nullptr);
          GPR_ASSERT(g_server != nullptr);
          {
            grpc_core::ExecCtx exec_ctx;
            grpc_channel_args_destroy(args);
          }
          grpc_server_register_completion_queue(g_server, cq, nullptr);
          grpc_server_start(g_server);
          server_shutdown = false;
          GPR_ASSERT(pending_server_shutdowns == 0);
        } else {
          end(&inp);
        }
        break;
      }
      // begin server shutdown
      case 5: {
        if (g_server != nullptr) {
          grpc_server_shutdown_and_notify(
              g_server, cq,
              create_validator(assert_success_and_decrement,
                               &pending_server_shutdowns));
          pending_server_shutdowns++;
          server_shutdown = true;
        } else {
          end(&inp);
        }
        break;
      }
      // cancel all calls if shutdown
      case 6: {
        if (g_server != nullptr && server_shutdown) {
          grpc_server_cancel_all_calls(g_server);
        } else {
          end(&inp);
        }
        break;
      }
      // destroy server
      case 7: {
        if (g_server != nullptr && server_shutdown &&
            pending_server_shutdowns == 0) {
          grpc_server_destroy(g_server);
          g_server = nullptr;
        } else {
          end(&inp);
        }
        break;
      }
      // check connectivity
      case 8: {
        if (g_channel != nullptr) {
          uint8_t try_to_connect = grpc_fuzzer_get_next_byte(&inp);
          if (try_to_connect == 0 || try_to_connect == 1) {
            grpc_channel_check_connectivity_state(g_channel, try_to_connect);
          } else {
            end(&inp);
          }
        } else {
          end(&inp);
        }
        break;
      }
      // watch connectivity
      case 9: {
        if (g_channel != nullptr) {
          grpc_connectivity_state st =
              grpc_channel_check_connectivity_state(g_channel, 0);
          if (st != GRPC_CHANNEL_SHUTDOWN) {
            gpr_timespec deadline = gpr_time_add(
                gpr_now(GPR_CLOCK_REALTIME),
                gpr_time_from_micros(grpc_fuzzer_get_next_uint32(&inp),
                                     GPR_TIMESPAN));
            grpc_channel_watch_connectivity_state(
                g_channel, st, deadline, cq,
                create_validator(validate_connectivity_watch,
                                 make_connectivity_watch(
                                     deadline, &pending_channel_watches)));
            pending_channel_watches++;
          }
        } else {
          end(&inp);
        }
        break;
      }
      // create a call
      case 10: {
        bool ok = true;
        if (g_channel == nullptr) ok = false;
        grpc_call* parent_call = nullptr;
        if (g_active_call->type != ROOT) {
          if (g_active_call->call == nullptr || g_active_call->type == CLIENT) {
            end(&inp);
            break;
          }
          parent_call = g_active_call->call;
        }
        uint32_t propagation_mask = grpc_fuzzer_get_next_uint32(&inp);
        grpc_slice method = read_string_like_slice(&inp);
        if (GRPC_SLICE_LENGTH(method) == 0) {
          ok = false;
        }
        grpc_slice host = read_string_like_slice(&inp);
        gpr_timespec deadline =
            gpr_time_add(gpr_now(GPR_CLOCK_REALTIME),
                         gpr_time_from_micros(grpc_fuzzer_get_next_uint32(&inp),
                                              GPR_TIMESPAN));

        if (ok) {
          call_state* cs = new_call(g_active_call, CLIENT);
          cs->call =
              grpc_channel_create_call(g_channel, parent_call, propagation_mask,
                                       cq, method, &host, deadline, nullptr);
        } else {
          end(&inp);
        }
        grpc_slice_unref(method);
        grpc_slice_unref(host);
        break;
      }
      // switch the 'current' call
      case 11: {
        g_active_call = g_active_call->next;
        break;
      }
      // queue some ops on a call
      case 12: {
        if (g_active_call->type == PENDING_SERVER ||
            g_active_call->type == ROOT || g_active_call->call == nullptr) {
          end(&inp);
          break;
        }
        size_t num_ops = grpc_fuzzer_get_next_byte(&inp);
        if (num_ops > 6) {
          end(&inp);
          break;
        }
        grpc_op* ops =
            static_cast<grpc_op*>(gpr_malloc(sizeof(grpc_op) * num_ops));
        if (num_ops > 0) memset(ops, 0, sizeof(grpc_op) * num_ops);
        bool ok = true;
        size_t i;
        grpc_op* op;
        uint8_t has_ops = 0;
        for (i = 0; i < num_ops; i++) {
          op = &ops[i];
          switch (grpc_fuzzer_get_next_byte(&inp)) {
            default:
              /* invalid value */
              op->op = (grpc_op_type)-1;
              ok = false;
              break;
            case GRPC_OP_SEND_INITIAL_METADATA:
              if (g_active_call->sent_initial_metadata) {
                ok = false;
              } else {
                g_active_call->sent_initial_metadata = true;
                op->op = GRPC_OP_SEND_INITIAL_METADATA;
                has_ops |= 1 << GRPC_OP_SEND_INITIAL_METADATA;
                read_metadata(&inp, &op->data.send_initial_metadata.count,
                              &op->data.send_initial_metadata.metadata,
                              g_active_call);
              }
              break;
            case GRPC_OP_SEND_MESSAGE:
              op->op = GRPC_OP_SEND_MESSAGE;
              if (g_active_call->send_message != nullptr) {
                ok = false;
              } else {
                has_ops |= 1 << GRPC_OP_SEND_MESSAGE;
                g_active_call->send_message =
                    op->data.send_message.send_message = read_message(&inp);
              }
              break;
            case GRPC_OP_SEND_CLOSE_FROM_CLIENT:
              op->op = GRPC_OP_SEND_CLOSE_FROM_CLIENT;
              has_ops |= 1 << GRPC_OP_SEND_CLOSE_FROM_CLIENT;
              break;
            case GRPC_OP_SEND_STATUS_FROM_SERVER:
              op->op = GRPC_OP_SEND_STATUS_FROM_SERVER;
              has_ops |= 1 << GRPC_OP_SEND_STATUS_FROM_SERVER;
              read_metadata(
                  &inp,
                  &op->data.send_status_from_server.trailing_metadata_count,
                  &op->data.send_status_from_server.trailing_metadata,
                  g_active_call);
              op->data.send_status_from_server.status =
                  static_cast<grpc_status_code>(
                      grpc_fuzzer_get_next_byte(&inp));
              op->data.send_status_from_server.status_details =
                  add_slice_to_unref(g_active_call,
                                     read_buffer_like_slice(&inp));
              break;
            case GRPC_OP_RECV_INITIAL_METADATA:
              op->op = GRPC_OP_RECV_INITIAL_METADATA;
              has_ops |= 1 << GRPC_OP_RECV_INITIAL_METADATA;
              op->data.recv_initial_metadata.recv_initial_metadata =
                  &g_active_call->recv_initial_metadata;
              break;
            case GRPC_OP_RECV_MESSAGE:
              if (g_active_call->done_flags & DONE_FLAG_CALL_CLOSED) {
                ok = false;
              } else {
                op->op = GRPC_OP_RECV_MESSAGE;
                has_ops |= 1 << GRPC_OP_RECV_MESSAGE;
                op->data.recv_message.recv_message =
                    &g_active_call->recv_message;
              }
              break;
            case GRPC_OP_RECV_STATUS_ON_CLIENT:
              op->op = GRPC_OP_RECV_STATUS_ON_CLIENT;
              op->data.recv_status_on_client.status = &g_active_call->status;
              op->data.recv_status_on_client.trailing_metadata =
                  &g_active_call->recv_trailing_metadata;
              op->data.recv_status_on_client.status_details =
                  &g_active_call->recv_status_details;
              break;
            case GRPC_OP_RECV_CLOSE_ON_SERVER:
              op->op = GRPC_OP_RECV_CLOSE_ON_SERVER;
              has_ops |= 1 << GRPC_OP_RECV_CLOSE_ON_SERVER;
              op->data.recv_close_on_server.cancelled =
                  &g_active_call->cancelled;
              break;
          }
          op->reserved = nullptr;
          op->flags = grpc_fuzzer_get_next_uint32(&inp);
        }
        if (g_channel == nullptr) ok = false;
        if (ok) {
          validator* v = make_finished_batch_validator(g_active_call, has_ops);
          g_active_call->pending_ops++;
          grpc_call_error error = grpc_call_start_batch(
              g_active_call->call, ops, num_ops, v, nullptr);
          if (error != GRPC_CALL_OK) {
            v->validate(v->arg, false);
            gpr_free(v);
          }
        } else {
          end(&inp);
        }
        if (!ok && (has_ops & (1 << GRPC_OP_SEND_MESSAGE))) {
          grpc_byte_buffer_destroy(g_active_call->send_message);
          g_active_call->send_message = nullptr;
        }
        gpr_free(ops);

        break;
      }
      // cancel current call
      case 13: {
        if (g_active_call->type != ROOT && g_active_call->call != nullptr) {
          grpc_call_cancel(g_active_call->call, nullptr);
        } else {
          end(&inp);
        }
        break;
      }
      // get a calls peer
      case 14: {
        if (g_active_call->type != ROOT && g_active_call->call != nullptr) {
          free_non_null(grpc_call_get_peer(g_active_call->call));
        } else {
          end(&inp);
        }
        break;
      }
      // get a channels target
      case 15: {
        if (g_channel != nullptr) {
          free_non_null(grpc_channel_get_target(g_channel));
        } else {
          end(&inp);
        }
        break;
      }
      // send a ping on a channel
      case 16: {
        if (g_channel != nullptr) {
          pending_pings++;
          grpc_channel_ping(g_channel, cq,
                            create_validator(decrement, &pending_pings),
                            nullptr);
        } else {
          end(&inp);
        }
        break;
      }
      // enable a tracer
      case 17: {
        char* tracer = grpc_fuzzer_get_next_string(&inp, nullptr);
        grpc_tracer_set_enabled(tracer, 1);
        gpr_free(tracer);
        break;
      }
      // disable a tracer
      case 18: {
        char* tracer = grpc_fuzzer_get_next_string(&inp, nullptr);
        grpc_tracer_set_enabled(tracer, 0);
        gpr_free(tracer);
        break;
      }
      // request a server call
      case 19: {
        if (g_server == nullptr) {
          end(&inp);
          break;
        }
        call_state* cs = new_call(g_active_call, PENDING_SERVER);
        cs->pending_ops++;
        validator* v = create_validator(finished_request_call, cs);
        grpc_call_error error =
            grpc_server_request_call(g_server, &cs->call, &cs->call_details,
                                     &cs->recv_initial_metadata, cq, cq, v);
        if (error != GRPC_CALL_OK) {
          v->validate(v->arg, false);
          gpr_free(v);
        }
        break;
      }
      // destroy a call
      case 20: {
        if (g_active_call->type != ROOT &&
            g_active_call->type != PENDING_SERVER &&
            g_active_call->call != nullptr) {
          destroy_call(g_active_call);
        } else {
          end(&inp);
        }
        break;
      }
      // resize the buffer pool
      case 21: {
        grpc_resource_quota_resize(g_resource_quota, read_uint22(&inp));
        break;
      }
      // create a secure channel
      case 22: {
        if (g_channel == nullptr) {
          char* target = grpc_fuzzer_get_next_string(&inp, nullptr);
          char* target_uri;
          gpr_asprintf(&target_uri, "dns:%s", target);
          grpc_channel_args* args = read_args(&inp);
          grpc_channel_credentials* creds = read_channel_creds(&inp);
          g_channel =
              grpc_secure_channel_create(creds, target_uri, args, nullptr);
          GPR_ASSERT(g_channel != nullptr);
          {
            grpc_core::ExecCtx exec_ctx;
            grpc_channel_args_destroy(args);
          }
          gpr_free(target_uri);
          gpr_free(target);
          grpc_channel_credentials_release(creds);
        } else {
          end(&inp);
        }
        break;
      }
    }
  }

  GPR_ASSERT(g_channel == nullptr);
  GPR_ASSERT(g_server == nullptr);
  GPR_ASSERT(g_active_call->type == ROOT);
  GPR_ASSERT(g_active_call->next == g_active_call);
  gpr_free(g_active_call);

  grpc_completion_queue_shutdown(cq);
  GPR_ASSERT(
      grpc_completion_queue_next(cq, gpr_inf_past(GPR_CLOCK_REALTIME), nullptr)
          .type == GRPC_QUEUE_SHUTDOWN);
  grpc_completion_queue_destroy(cq);

  grpc_resource_quota_unref(g_resource_quota);

  grpc_shutdown_blocking();
  return 0;
}
