# 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 'spec_helper'

module ElasticAPM
  RSpec.describe Metrics do
    let(:config) { Config.new }
    let(:callback) { ->(_) {} }

    describe 'life cycle' do
      subject { described_class.new(config, &callback) }

      describe '#start' do
        before { subject.start }
        it { should be_running }
      end

      describe '#stop' do
        it 'stops the collector' do
          subject.start
          subject.stop
          expect(subject).to_not be_running
        end
      end

      describe 'stop and start again' do
        before do
          subject.start
          subject.stop
        end
        after { subject.stop }

        it 'restarts collecting metrics' do
          subject.start
          expect(subject.instance_variable_get(:@timer_task)).to be_running
        end
      end

      context 'when disabled' do
        let(:config) { Config.new metrics_interval: '0s' }

        it "doesn't start" do
          subject.start
          expect(subject).to_not be_running
          subject.stop
          expect(subject).to_not be_running
        end
      end
    end

    describe '.new' do
      subject { described_class.new(config, &callback) }
      it { should be_a Metrics::Registry }
    end

    describe '.collect' do
      subject { described_class.new(config, &callback) }
      after { subject.stop }

      it 'samples all samplers' do
        subject.define_sets
        subject.sets.each_value do |sampler|
          expect(sampler).to receive(:collect).at_least(:once)
        end
        subject.start
        subject.collect
      end
    end

    describe '.collect_and_send' do
      context 'when samples' do
        subject { described_class.new(config, &callback) }
        let(:callback) { ->(_) {} }
        before { subject.start }
        after { subject.stop }

        it 'calls callback' do
          subject.collect_and_send # disable on unsupported jruby
          next unless subject.sets.values.select { |s| s.metrics.any? }.any?

          expect(callback).to receive(:call).with(Metricset).at_least(1)
          subject.collect_and_send
        end
      end

      context 'when no samples' do
        it 'calls callback' do
          callback = ->(_) {}
          subject = described_class.new(config, &callback)
          subject.define_sets
          subject.sets.each_value do |sampler|
            expect(sampler).to receive(:collect).at_least(:once) { nil }
          end
          subject.start
          expect(callback).to_not receive(:call)

          subject.collect_and_send
          subject.stop
        end
      end

      context 'when recording is false' do
        subject { described_class.new(config, &callback) }
        let(:callback) { ->(_) {} }
        let(:config) { Config.new(recording: false) }
        it 'does not collect metrics' do
          expect(subject).to_not receive(:collect)
          subject.collect_and_send
        end
      end
    end

    xcontext 'thread safety stress test', :mock_intake do
      it 'handles multiple threads reporting and collecting at the same time' do
        thread_count = 1_000

        names = Array.new(5).map do
          SecureRandom.hex(5)
        end

        with_agent(metrics_interval: '100ms') do
          metrics = ElasticAPM.agent.metrics

          Array.new(thread_count).map do
            Thread.new do
              metrics.get(:breakdown).counter('a').inc!
              metrics.get(:breakdown).counter('b').inc!
              metrics.get(:breakdown).counter('c').dec!
              metrics.get(:transaction).counter(
                :a_with_tags,
                tags: { 'name': names.sample },
                reset_on_collect: true
              ).inc!
              metrics.get(:transaction).counter(
                :b_with_tags,
                tags: { 'name': names.sample },
                reset_on_collect: true
              ).inc!
              metrics.get(:transaction).counter(
                :c_with_tags,
                tags: { 'name': names.sample },
                reset_on_collect: true
              ).inc!

              sleep 0.15 # longer than metrics_interval
            end
          end.each(&:join)
        end

        samples =
          @mock_intake.metricsets.each_with_object({}) do |set, result|
            result.merge! set['samples']
          end

        expect(samples['a']['value']).to eq(thread_count)
        expect(samples['b']['value']).to eq(thread_count)
        expect(samples['c']['value']).to eq(0 - thread_count)

        expect(samples['a_with_tags']['value']).to be > 0
        expect(samples['b_with_tags']['value']).to be > 0
        expect(samples['c_with_tags']['value']).to be > 0
      end
    end

    describe '#handle_forking!' do
      subject { described_class.new(config, &callback) }
      before do
        subject.handle_forking!
      end
      after { subject.stop }

      it 'restarts the TimerTask' do
        expect(subject.instance_variable_get(:@timer_task)).to be_running
      end

      context 'when not collecting metrics' do
        let(:config) { Config.new(metrics_interval: 0) }

        it 'does not create a TimerTask' do
          expect(subject.instance_variable_get(:@timer_task)).to be nil
        end
      end
    end
  end
end
