# 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 CentralConfig do
    let(:config) do
      Config.new(
        central_config: true,
        service_name: 'MyApp',
        log_level: Logger::DEBUG
      )
    end
    subject { described_class.new(config) }

    describe '#start' do
      it 'polls for config' do
        req_stub = stub_response({ transaction_sample_rate: '0.5' })
        subject.start
        subject.promise.wait
        expect(req_stub).to have_been_requested.at_least_once
        subject.stop
      end

      context 'with complex service name' do
        let(:config) do
          Config.new(
            central_config: true,
            service_name: 'My app with +-_',
            log_level: Logger::DEBUG
          )
        end

        it 'escapes chars' do
          req_stub = stub_response({}, service_name: 'My%20app%20with%20%2B-_')
          subject.start
          subject.promise.wait
          expect(req_stub).to have_been_requested.at_least_once
          subject.stop
        end
      end

      context 'environment' do
        let(:config) do
          Config.new(
            central_config: true,
            service_name: 'MyApp',
            environment: 'staging'
          )
        end

        it 'includes env in params' do
          req_stub = stub_response({}, environment: 'staging')
          subject.start
          subject.promise.wait
          expect(req_stub).to have_been_requested.at_least_once
          subject.stop
        end
      end

      context 'when disabled' do
        let(:config) { Config.new(central_config: false) }

        it 'does nothing' do
          req_stub = stub_response({ transaction_sample_rate: '0.5' })
          subject.start
          expect(subject.promise).to be nil
          expect(req_stub).to_not have_been_requested
          subject.stop
        end
      end
    end

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

      it 'restarts fetching the config' do
        req_stub = stub_response({ transaction_sample_rate: '0.5' })
        subject.start
        subject.promise.wait
        expect(req_stub).to have_been_requested.at_least_once
      end
    end

    describe '#fetch_and_apply_config' do
      it 'queries APM Server and applies config' do
        req_stub = stub_response({ transaction_sample_rate: '0.5' })
        expect(config.logger).to receive(:info)
        expect(config.logger).to receive(:debug).twice

        subject.fetch_and_apply_config
        subject.promise.wait

        # why more times, sometimes?
        expect(req_stub).to have_been_requested.at_least_once
        expect(subject.config.transaction_sample_rate).to eq(0.5)
      end

      it 'reverts config if later 404' do
        stub_response({ transaction_sample_rate: '0.5' })

        subject.fetch_and_apply_config
        subject.promise.wait

        stub_response('Not found', response: { status: 404 })

        subject.fetch_and_apply_config
        subject.promise.wait

        expect(subject.config.transaction_sample_rate).to eq(1.0)
      end

      context 'when server responds 200 and cache-control' do
        it 'schedules a new poll' do
          stub_response(
            {},
            response: {
              headers: { 'Cache-Control': 'must-revalidate, max-age=123' }
            }
          )

          subject.fetch_and_apply_config
          subject.promise.wait

          expect(subject.scheduled_task).to be_pending
          expect(subject.scheduled_task.initial_delay).to eq 123
        end
      end

      context 'when server responds 304' do
        it 'doesn\'t restore config, schedules a new poll' do
          stub_response(
            { transaction_sample_rate: 0.5 },
            response: {
              headers: { 'Cache-Control': 'must-revalidate, max-age=0.1' }
            }
          )

          subject.fetch_and_apply_config
          subject.promise.wait

          stub_response(
            nil,
            response: {
              status: 304,
              headers: { 'Cache-Control': 'must-revalidate, max-age=123' }
            }
          )

          subject.fetch_and_apply_config
          subject.promise.wait

          expect(subject.scheduled_task).to be_pending
          expect(subject.scheduled_task.initial_delay).to eq 123
          expect(subject.config.transaction_sample_rate).to eq(0.5)
        end
      end

      context 'when server sends etag header' do
        it 'includes etag in next request' do
          stub_response(
            nil,
            response: { headers: { 'Etag': '___etag___' } }
          )

          subject.fetch_and_apply_config
          subject.promise.wait

          stub_response(
            nil,
            request: { headers: { 'If-None-Match': '___etag___' } },
            response: { headers: { 'Etag': '___etag___' } }
          )

          subject.fetch_and_apply_config
          subject.promise.wait
        end
      end

      context 'when server responds 404' do
        it 'schedules a new poll' do
          stub_response('Not found', response: { status: 404 })

          subject.fetch_and_apply_config
          subject.promise.wait

          expect(subject.scheduled_task).to be_pending
          expect(subject.scheduled_task.initial_delay).to eq 300
        end
      end

      context 'when there is a network error' do
        it 'schedules a new poll' do
          stub_response(nil, error: HTTP::ConnectionError)

          subject.fetch_and_apply_config
          subject.promise.wait

          expect(subject.scheduled_task).to be_pending
          expect(subject.scheduled_task.initial_delay).to eq 300
        end
      end
    end

    describe '#fetch_config' do
      context 'when successful' do
        it 'returns response object' do
          stub_response({ ok: 1 })

          expect(subject.fetch_config).to be_a(HTTP::Response)
        end
      end

      context 'when not found' do
        before do
          stub_response('Not found', response: { status: 404 })
        end

        it 'raises an error' do
          expect { subject.fetch_config }
            .to raise_error(CentralConfig::ClientError)
        end

        it 'includes the response' do
          begin
            subject.fetch_config
          rescue CentralConfig::ClientError => e
            expect(e.response).to be_a(HTTP::Response)
          end
        end
      end

      context 'when server error' do
        it 'raises an error' do
          stub_response('Server error', response: { status: 500 })

          expect { subject.fetch_config }
            .to raise_error(CentralConfig::ServerError)
        end
      end

      context 'with a secret token' do
        before { config.secret_token = 'zecret' }

        it 'sets auth header' do
          stub_response(
            {},
            request: { headers: { 'Authorization': 'Bearer zecret' } }
          )

          subject.fetch_config
        end
      end

      context 'with an api key' do
        before do
          config.api_key = 'a_base64_encoded_string'
        end

        it 'sets auth header' do
          stub_response(
            {},
            request: {
              headers: {
                'Authorization': 'ApiKey a_base64_encoded_string'
              }
            }
          )

          subject.fetch_config
        end
      end
    end

    describe '#assign' do
      it 'updates config' do
        subject.assign(transaction_sample_rate: 0.5)
        expect(subject.config.transaction_sample_rate).to eq(0.5)
      end

      it 'reverts to previous when missing' do
        subject.assign(transaction_sample_rate: 0.5)
        subject.assign({})
        expect(subject.config.transaction_sample_rate).to eq(1.0)
      end

      it 'goes back and forth' do
        subject.assign(transaction_sample_rate: 0.5)
        subject.assign({})
        subject.assign(transaction_sample_rate: 0.5)
        expect(subject.config.transaction_sample_rate).to eq(0.5)
      end

      describe 'log level' do
        it 'maps `trace` to `debug`' do
          subject.assign(log_level: 'trace')
          expect(subject.config.log_level).to eq(Logger::DEBUG)
        end

        it 'maps `critical` to `fatal`' do
          subject.assign(log_level: 'critical')
          expect(subject.config.log_level).to eq(Logger::FATAL)
        end

        it 'maps `off` to `fatal`' do
          subject.assign(log_level: 'off')
          expect(subject.config.log_level).to eq(Logger::FATAL)
        end

        it 'maps `debug` to `debug`' do
          subject.assign(log_level: 'debug')
          expect(subject.config.log_level).to eq(Logger::DEBUG)
        end
      end
    end

    describe '#handle_forking!' do
      it 'reschedules the scheduled task' do
        req_stub = stub_response({ transaction_sample_rate: '0.5' })

        subject.handle_forking!
        subject.promise.wait
        expect(subject.scheduled_task).to be_pending
        expect(req_stub).to have_been_requested.at_least_once

        subject.stop
      end
    end

    def stub_response(body, request: {}, response: {}, error: nil, service_name: "MyApp", environment: ENV['RAILS_ENV'])
      url = "http://localhost:8200/config/v1/agents" \
        "?service.name=#{service_name}" \
        "&service.environment=#{environment}"

      return stub_request(:get, url).to_raise(error) if error

      stub_request(:get, url).tap do |stub|
        stub.with(request) if request.any?
        stub.to_return(body: body&.to_json, **response)
      end
    end
  end
end
