# Licensed to Elasticsearch B.V. under one or more contributor
# license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright
# ownership. Elasticsearch B.V. 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.

# frozen_string_literal: true

require 'integration_helper'

require 'mongo'

module ElasticAPM
  RSpec.describe 'Spy: MongoDB' do
    let(:url) do
      ENV.fetch('MONGODB_URL', '127.0.0.1:27017')
    end

    it 'instruments db admin commands', :intercept do
      with_agent do
        client =
          Mongo::Client.new(
            [url],
            database: 'elastic-apm-test',
            logger: Logger.new(nil),
            server_selection_timeout: 5
          )

        ElasticAPM.with_transaction 'Mongo test' do
          client.database.collections
        end

        client.close
      end

      span, = @intercepted.spans

      expect(span.name).to eq 'elastic-apm-test.listCollections'
      expect(span.type).to eq 'db'
      expect(span.subtype).to eq 'mongodb'
      expect(span.action).to eq 'query'
      expect(span.duration).to_not be_nil
      expect(span.outcome).to eq 'success'

      db = span.context.db
      expect(db.instance).to eq 'elastic-apm-test'
      expect(db.type).to eq 'mongodb'
      expect(db.statement).to include('{"listCollections"=>1, "cursor"=>{}, ' \
        '"nameOnly"=>true')
                          .or include('{"listCollections" => 1, "cursor" => {}, ' \
        '"nameOnly" => true')
      expect(db.user).to be nil
    end

    it 'instruments commands on collections', :intercept do
      with_agent do
        client =
          Mongo::Client.new(
            [url],
            database: 'elastic-apm-test',
            logger: Logger.new(nil),
            server_selection_timeout: 5
          )

        client['testing'].drop
        client['testing'].create
        ElasticAPM.with_transaction 'Mongo test' do
          client['testing'].delete_many
        end

        client['testing'].drop
        client.close
      end

      span, = @intercepted.spans

      expect(span.name).to eq 'elastic-apm-test.testing.delete'
      expect(span.type).to eq 'db'
      expect(span.subtype).to eq 'mongodb'
      expect(span.action).to eq 'query'
      expect(span.duration).to_not be_nil

      db = span.context.db
      expect(db.instance).to eq 'elastic-apm-test'
      expect(db.type).to eq 'mongodb'
      expect(db.statement).to match('"delete"=>"testing"')
                          .or match('"delete" => "testing"')
      expect(db.user).to be nil
    end

    it 'instruments commands with special BSON types', :intercept do
      with_agent do
        client =
          Mongo::Client.new(
            [url],
            database: 'elastic-apm-test',
            logger: Logger.new(nil),
            server_selection_timeout: 5
          )

        ElasticAPM.with_transaction 'Mongo test' do
          client['testing'].find(a: BSON::Decimal128.new('1')).to_a
        end

        client.close
      end

      span, = @intercepted.spans

      expect(span.name).to eq 'elastic-apm-test.testing.find'
      expect(span.type).to eq 'db'
      expect(span.subtype).to eq 'mongodb'
      expect(span.action).to eq 'query'
      expect(span.duration).to_not be_nil

      db = span.context.db
      expect(db.instance).to eq 'elastic-apm-test'
      expect(db.type).to eq 'mongodb'
      expect(db.statement).to include('{"a"=>BSON::Decimal128(\'1\')}')
                          .or include('{"a" => BSON::Decimal128(\'1\')}')
      expect(db.user).to be nil
    end

    it 'instruments getMore comments', :intercept do
      with_agent do
        client =
          Mongo::Client.new(
            [url],
            database: 'elastic-apm-test',
            logger: Logger.new(nil),
            server_selection_timeout: 5
          )

        3.times { |i| client['testing'].insert_one(a: i) }
        ElasticAPM.with_transaction 'Mongo test' do
          client['testing'].find({}, batch_size: 2).to_a
        end

        client.close
      end

      find_span, get_more_span = @intercepted.spans

      expect(find_span.name).to eq 'elastic-apm-test.testing.find'
      expect(find_span.type).to eq 'db'
      expect(find_span.subtype).to eq 'mongodb'
      expect(find_span.action).to eq 'query'
      expect(find_span.duration).to_not be_nil

      db = find_span.context.db
      expect(db.instance).to eq 'elastic-apm-test'
      expect(db.type).to eq 'mongodb'
      expect(db.statement).to include('{"find"=>"testing"')
                          .or include('{"find" => "testing"')
      expect(db.user).to be nil

      expect(get_more_span.name).to eq 'elastic-apm-test.testing.getMore'
      expect(get_more_span.type).to eq 'db'
      expect(get_more_span.subtype).to eq 'mongodb'
      expect(get_more_span.action).to eq 'query'
      expect(get_more_span.duration).to_not be_nil

      db = get_more_span.context.db
      expect(db.instance).to eq 'elastic-apm-test'
      expect(db.type).to eq 'mongodb'
      expect(db.statement).to include('{"getMore"=>#<BSON::Int64')
                          .or include('{"getMore" => #<BSON::Int64')
      expect(db.user).to be nil
    end

    it 'sets outcome to `failure` for failed operations', :intercept do
      with_agent do
        client =
          Mongo::Client.new(
            [url],
            database: 'elastic-apm-test',
            logger: Logger.new(nil),
            server_selection_timeout: 1
          )

        ElasticAPM.with_transaction 'Mongo test' do
          begin
            client['testing'].find('$or' => true).to_a
          rescue
          end
        end

        client.close
      end

      span, = @intercepted.spans

      expect(span.outcome).to eq 'failure'
    end
  end
end
