# Copyright 2015 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.

require 'spec_helper'

include GRPC::Core

shared_examples 'basic GRPC message delivery is OK' do
  include GRPC::Core

  context 'the test channel' do
    it 'should have a target' do
      expect(@ch.target).to be_a(String)
    end
  end

  it 'unary calls work' do
    run_services_on_server(@server, services: [EchoService]) do
      call = @stub.an_rpc(EchoMsg.new, return_op: true)
      expect(call.execute).to be_a(EchoMsg)
    end
  end

  it 'unary calls work when enabling compression' do
    run_services_on_server(@server, services: [EchoService]) do
      long_request_str = '0' * 2000
      md = { 'grpc-internal-encoding-request' => 'gzip' }
      call = @stub.an_rpc(EchoMsg.new(msg: long_request_str),
                          return_op: true,
                          metadata: md)
      response = call.execute
      expect(response).to be_a(EchoMsg)
      expect(response.msg).to eq(long_request_str)
    end
  end

  def client_cancel_test(cancel_proc, expected_code,
                         expected_details)
    call = @stub.an_rpc(EchoMsg.new, return_op: true)
    run_services_on_server(@server, services: [EchoService]) do
      # start the call, but don't send a message yet
      call.start_call
      # cancel the call
      cancel_proc.call(call)
      # check the client's status
      failed = false
      begin
        call.execute
      rescue GRPC::BadStatus => e
        failed = true
        expect(e.code).to be expected_code
        expect(e.details).to eq expected_details
      end
      expect(failed).to be(true)
    end
  end

  it 'clients can cancel a call on the server' do
    expected_code = StatusCodes::CANCELLED
    expected_details = 'CANCELLED'
    cancel_proc = proc(&:cancel)
    client_cancel_test(cancel_proc, expected_code, expected_details)
  end

  it 'cancel_with_status unknown status' do
    code = StatusCodes::UNKNOWN
    details = 'test unknown reason'
    cancel_proc = proc { |call| call.cancel_with_status(code, details) }
    client_cancel_test(cancel_proc, code, details)
  end

  it 'cancel_with_status unknown status' do
    code = StatusCodes::FAILED_PRECONDITION
    details = 'test failed precondition reason'
    cancel_proc = proc { |call| call.cancel_with_status(code, details) }
    client_cancel_test(cancel_proc, code, details)
  end
end

shared_examples 'GRPC metadata delivery works OK' do
  describe 'from client => server' do
    before(:example) do
      n = 7  # arbitrary number of metadata
      diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] }
      diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }]
      null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] }
      null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }]
      same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] }
      same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }]
      symbol_key = { a_key: 'a val' }
      @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key]
      @bad_keys = []
      @bad_keys << { Object.new => 'a value' }
      @bad_keys << { 1 => 'a value' }
    end

    it 'raises an exception if a metadata key is invalid' do
      @bad_keys.each do |md|
        # NOTE: no need to run a server in this test b/c the failure
        # happens while validating metadata to send.
        failed = false
        begin
          @stub.an_rpc(EchoMsg.new, metadata: md)
        rescue TypeError => e
          failed = true
          expect(e.message).to eq('grpc_rb_md_ary_fill_hash_cb: bad type for key parameter')
        end
        expect(failed).to be(true)
      end
    end

    it 'sends all the metadata pairs when keys and values are valid' do
      service = EchoService.new
      run_services_on_server(@server, services: [service]) do
        @valid_metadata.each_with_index do |md, i|
          expect(@stub.an_rpc(EchoMsg.new, metadata: md)).to be_a(EchoMsg)
          # confirm the server can receive the client metadata
          # finish the call
          expect(service.received_md.length).to eq(i + 1)
          md.each do |k, v|
            expect(service.received_md[i][k.to_s]).to eq(v)
          end
        end
      end
    end
  end

  describe 'from server => client' do
    before(:example) do
      n = 7  # arbitrary number of metadata
      diff_keys_fn = proc { |i| [format('k%d', i), format('v%d', i)] }
      diff_keys = Hash[n.times.collect { |x| diff_keys_fn.call x }]
      null_vals_fn = proc { |i| [format('k%d', i), format('v\0%d', i)] }
      null_vals = Hash[n.times.collect { |x| null_vals_fn.call x }]
      same_keys_fn = proc { |i| [format('k%d', i), [format('v%d', i)] * n] }
      same_keys = Hash[n.times.collect { |x| same_keys_fn.call x }]
      symbol_key = { a_key: 'a val' }
      @valid_metadata = [diff_keys, same_keys, null_vals, symbol_key]
      @bad_keys = []
      @bad_keys << { Object.new => 'a value' }
      @bad_keys << { 1 => 'a value' }
    end

    it 'raises an exception if a metadata key is invalid' do
      service = EchoService.new
      run_services_on_server(@server, services: [service]) do
        @bad_keys.each do |md|
          proceed = Queue.new
          server_exception = nil
          service.on_call_started = proc do |call|
            call.send_initial_metadata(md)
          rescue TypeError => e
            server_exception = e
          ensure
            proceed.push(1)
          end
          client_exception = nil
          client_call = @stub.an_rpc(EchoMsg.new, return_op: true)
          thr = Thread.new do
            client_call.execute
          rescue GRPC::BadStatus => e
            client_exception = e
          end
          proceed.pop
          # TODO(apolcyn): we shouldn't need this cancel here. It's
          # only currently needed b/c the server does not seem to properly
          # terminate the RPC if it fails to send initial metadata. That
          # should be fixed, in which case this cancellation can be removed.
          client_call.cancel
          thr.join
          p client_exception
          expect(client_exception.nil?).to be(false)
          expect(server_exception.nil?).to be(false)
          expect(server_exception.message).to eq(
            'grpc_rb_md_ary_fill_hash_cb: bad type for key parameter')
        end
      end
    end

    it 'sends an empty hash if no metadata is added' do
      run_services_on_server(@server, services: [EchoService]) do
        call = @stub.an_rpc(EchoMsg.new, return_op: true)
        expect(call.execute).to be_a(EchoMsg)
        expect(call.metadata).to eq({})
      end
    end

    it 'sends all the pairs when keys and values are valid' do
      service = EchoService.new
      run_services_on_server(@server, services: [service]) do
        @valid_metadata.each do |md|
          service.on_call_started = proc do |call|
            call.send_initial_metadata(md)
          end
          call = @stub.an_rpc(EchoMsg.new, return_op: true)
          call.execute
          replace_symbols = Hash[md.each_pair.collect { |x, y| [x.to_s, y] }]
          expect(call.metadata).to eq(replace_symbols)
        end
      end
    end
  end
end

describe 'the http client/server' do
  before(:example) do
    server_host = '0.0.0.0:0'
    @server = new_rpc_server_for_testing
    server_port = @server.add_http2_port(server_host, :this_port_is_insecure)
    @ch = Channel.new("0.0.0.0:#{server_port}", nil, :this_channel_is_insecure)
    @stub = EchoStub.new(
      "0.0.0.0:#{server_port}", nil, channel_override: @ch)
  end

  it_behaves_like 'basic GRPC message delivery is OK' do
  end

  it_behaves_like 'GRPC metadata delivery works OK' do
  end
end

describe 'the secure http client/server' do
  def load_test_certs
    test_root = File.join(File.dirname(__FILE__), 'testdata')
    files = ['ca.pem', 'server1.key', 'server1.pem']
    files.map { |f| File.open(File.join(test_root, f)).read }
  end

  before(:example) do
    certs = load_test_certs
    server_host = '0.0.0.0:0'
    server_creds = GRPC::Core::ServerCredentials.new(
      nil, [{ private_key: certs[1], cert_chain: certs[2] }], false)
    @server = new_rpc_server_for_testing
    server_port = @server.add_http2_port(server_host, server_creds)
    args = { Channel::SSL_TARGET => 'foo.test.google.fr' }
    @ch = Channel.new(
      "0.0.0.0:#{server_port}", args,
      GRPC::Core::ChannelCredentials.new(certs[0], nil, nil))
    @stub = EchoStub.new(
      "0.0.0.0:#{server_port}", nil, channel_override: @ch)
  end

  it_behaves_like 'basic GRPC message delivery is OK' do
  end

  it_behaves_like 'GRPC metadata delivery works OK' do
  end

  it 'modifies metadata with CallCredentials' do
    # create call creds
    auth_proc = proc { { 'k1' => 'v1' } }
    call_creds = GRPC::Core::CallCredentials.new(auth_proc)
    # create arbitrary custom metadata
    custom_md = { 'k2' => 'v2' }
    # perform an RPC
    echo_service = EchoService.new
    run_services_on_server(@server, services: [echo_service]) do
      expect(@stub.an_rpc(EchoMsg.new,
                          credentials: call_creds,
                          metadata: custom_md)).to be_a(EchoMsg)
    end
    # call creds metadata should be merged with custom MD
    expect(echo_service.received_md.length).to eq(1)
    expected_md = { 'k1' => 'v1', 'k2' => 'v2' }
    expected_md.each do |k, v|
      expect(echo_service.received_md[0][k]).to eq(v)
    end
  end

  it 'plugin procs survive GC after their CallCredentials wrapper is collected' do
    # Regression test for use-after-free in grpc_metadata_credentials_plugin.
    # When CallCredentials are composed via a.compose(b), the resulting
    # wrapper marks b but NOT a. If a's Ruby wrapper is GC'd, the C-level
    # composite still holds a's plugin. Without the pin_plugin_proc fix,
    # a's proc would be collected, and invoking the callback would crash
    # with a segfault (rb_id_table_lookup on freed memory).
    first_call_count = 0

    first_creds = GRPC::Core::CallCredentials.new(proc {
      first_call_count += 1
      { 'first-key' => 'first-value' }
    })
    second_creds = GRPC::Core::CallCredentials.new(proc {
      { 'second-key' => 'second-value' }
    })
    composed = first_creds.compose(second_creds)

    # Drop references. composed.mark includes second_creds but NOT first_creds,
    # so first_creds' Ruby wrapper becomes GC-eligible while its C plugin
    # survives inside the composite.
    first_creds = nil # rubocop:disable Lint/UselessAssignment
    second_creds = nil # rubocop:disable Lint/UselessAssignment

    # Force GC to collect the first_creds wrapper. Without the fix, the proc
    # from first_creds would also be collected here.
    GC.start(full_mark: true, immediate_sweep: true)

    # Make an RPC with the composed credentials — both plugin callbacks fire.
    # Without the fix, accessing the freed proc would crash the process.
    echo_service = EchoService.new
    run_services_on_server(@server, services: [echo_service]) do
      expect(@stub.an_rpc(EchoMsg.new,
                          credentials: composed)).to be_a(EchoMsg)
    end
    expect(first_call_count).to eq(1)
    expect(echo_service.received_md[0]['first-key']).to eq('first-value')
    expect(echo_service.received_md[0]['second-key']).to eq('second-value')
  end

  it 'modifies large metadata with CallCredentials' do
    val_array = %w(
      '00000000000000000000000000000000000000000000000000000000000000',
      '11111111111111111111111111111111111111111111111111111111111111',
    )
    # create call creds
    auth_proc = proc do
      {
        k2: val_array,
        k3: '0000000000000000000000000000000000000000000000000000000000',
        keeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey4: 'v4'
      }
    end
    call_creds = GRPC::Core::CallCredentials.new(auth_proc)
    # create arbitrary custom metadata
    custom_md = { k1: 'v1' }
    # perform an RPC
    echo_service = EchoService.new
    run_services_on_server(@server, services: [echo_service]) do
      expect(@stub.an_rpc(EchoMsg.new,
                          credentials: call_creds,
                          metadata: custom_md)).to be_a(EchoMsg)
    end
    # call creds metadata should be merged with custom MD
    expect(echo_service.received_md.length).to eq(1)
    expected_md = {
      k1: 'v1',
      k2: val_array,
      k3: '0000000000000000000000000000000000000000000000000000000000',
      keeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeey4: 'v4'
    }
    expected_md.each do |k, v|
      expect(echo_service.received_md[0][k.to_s]).to eq(v)
    end
  end
end
