# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require_relative '../../../test_helper'
require 'new_relic/agent/attributes'
require 'new_relic/agent/attribute_filter'

class AttributesTest < Minitest::Test
  Attributes = NewRelic::Agent::Attributes
  AttributeFilter = NewRelic::Agent::AttributeFilter

  def setup
    # Lots of tests that just want the default behavior, so make sure filter
    # is updated to that base
    NewRelic::Agent.instance.refresh_attribute_filter
  end

  def test_adds_custom_attribute
    attributes = create_attributes
    attributes.merge_custom_attributes(:foo => 'bar')

    assert_equal({'foo' => 'bar'}, attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER))
  end

  def test_disable_custom_attributes
    with_config({:'transaction_tracer.attributes.enabled' => false}) do
      attributes = create_attributes
      attributes.merge_custom_attributes(:foo => 'bar')

      assert_empty attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    end
  end

  def test_disable_custom_attributes_in_high_security_mode
    with_config(:high_security => true) do
      attributes = create_attributes
      attributes.merge_custom_attributes(:foo => 'bar')

      assert_empty attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    end
  end

  def test_disable_merging_custom_attributes_in_high_security_mode
    with_config(:high_security => true) do
      attributes = create_attributes
      attributes.merge_custom_attributes(:foo => 'bar')

      assert_empty attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    end
  end

  def test_merge_custom_attributes
    attributes = create_attributes
    params = {:foo => {:bar => 'baz'}}
    attributes.merge_custom_attributes(params)

    assert_equal({'foo.bar' => 'baz'}, attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER))
  end

  def test_adds_agent_attribute
    attributes = create_attributes
    attributes.add_agent_attribute(:foo, 'bar', AttributeFilter::DST_ALL)

    assert_equal({:foo => 'bar'}, attributes.agent_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER))
  end

  def test_disable_agent_attributes
    with_config({:'transaction_tracer.attributes.enabled' => false}) do
      attributes = create_attributes
      attributes.add_agent_attribute(:foo, 'bar', AttributeFilter::DST_ALL)

      assert_empty attributes.agent_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    end
  end

  def test_agent_attributes_obey_default_destinations
    attributes = create_attributes
    attributes.add_agent_attribute(:foo, 'bar', AttributeFilter::DST_ERROR_COLLECTOR)

    assert_empty attributes.agent_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
  end

  def test_adds_intrinsic_attribute_to_only_traces_and_errors
    attributes = create_attributes
    attributes.add_intrinsic_attribute(:foo, 'bar')

    expected = {:foo => 'bar'}

    assert_equal(expected, attributes.intrinsic_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER))
    assert_equal(expected, attributes.intrinsic_attributes_for(AttributeFilter::DST_ERROR_COLLECTOR))

    assert_empty(attributes.intrinsic_attributes_for(AttributeFilter::DST_TRANSACTION_EVENTS))
    assert_empty(attributes.intrinsic_attributes_for(AttributeFilter::DST_BROWSER_MONITORING))
  end

  def test_intrinsic_attributes_arent_disabled_for_traces_and_errors
    with_config({:'transaction_tracer.attributes.enabled' => false}) do
      attributes = create_attributes
      attributes.add_intrinsic_attribute(:foo, 'bar')

      expected = {:foo => 'bar'}

      assert_equal(expected, attributes.intrinsic_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER))
      assert_equal(expected, attributes.intrinsic_attributes_for(AttributeFilter::DST_ERROR_COLLECTOR))

      assert_empty(attributes.intrinsic_attributes_for(AttributeFilter::DST_TRANSACTION_EVENTS))
      assert_empty(attributes.intrinsic_attributes_for(AttributeFilter::DST_BROWSER_MONITORING))
    end
  end

  MULTIBYTE_CHARACTER = '七'

  def test_truncates_multibyte_string
    # Leading single byte character makes byteslice yield invalid string
    value = 'j' + MULTIBYTE_CHARACTER * 1000

    attributes = create_attributes
    attributes.merge_custom_attributes(:key => value)

    custom_attributes = attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    result = custom_attributes['key']

    assert_predicate result, :valid_encoding?
    assert result.bytesize < NewRelic::Agent::Attributes::VALUE_LIMIT
  end

  def test_truncates_multibyte_symbol
    # Leading single byte character makes byteslice yield invalid string
    value = ('j' + MULTIBYTE_CHARACTER * 1000).to_sym

    attributes = create_attributes
    attributes.merge_custom_attributes(:key => value)

    custom_attributes = attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
    result = custom_attributes['key']

    assert_predicate result, :valid_encoding?
    assert result.bytesize < NewRelic::Agent::Attributes::VALUE_LIMIT
  end

  def test_limits_key_length
    key = 'x' * (Attributes::KEY_LIMIT + 1)
    expects_logging(:warn, includes(key))

    attributes = create_attributes
    attributes.merge_custom_attributes(key => '')

    assert_custom_attributes_empty(attributes)
  end

  def test_limits_key_length_by_bytes
    key = MULTIBYTE_CHARACTER * Attributes::KEY_LIMIT
    expects_logging(:warn, includes(key))

    attributes = create_attributes
    attributes.merge_custom_attributes(key => '')

    assert_custom_attributes_empty(attributes)
  end

  def test_limits_key_length_symbol
    key = ('x' * (Attributes::KEY_LIMIT + 1)).to_sym
    expects_logging(:warn, includes(key.to_s))

    attributes = create_attributes
    attributes.merge_custom_attributes(key => '')

    assert_custom_attributes_empty(attributes)
  end

  def test_limits_key_length_on_merge_custom_attributes
    key = ('x' * (Attributes::KEY_LIMIT + 1)).to_sym
    expects_logging(:warn, includes(key.to_s))

    attributes = create_attributes
    attributes.merge_custom_attributes(key => '')

    assert_custom_attributes_empty(attributes)
  end

  def test_allows_non_string_key_type
    attributes = create_attributes
    attributes.merge_custom_attributes(1 => 'value')

    assert_equal 'value', custom_attributes(attributes)['1']
  end

  def test_truncates_string_values
    value = 'x' * 1000

    attributes = create_attributes
    attributes.merge_custom_attributes(:key => value)

    assert_equal Attributes::VALUE_LIMIT, custom_attributes(attributes)['key'].length
  end

  def test_truncates_symbol_values
    value = ('x' * 1000).to_sym

    attributes = create_attributes
    attributes.merge_custom_attributes(:key => value)

    assert_equal Attributes::VALUE_LIMIT, custom_attributes(attributes)['key'].length
  end

  def test_leaves_numbers_alone
    attributes = create_attributes
    attributes.merge_custom_attributes(:key => 42)

    assert_equal 42, custom_attributes(attributes)['key']
  end

  def test_limits_attribute_count
    ::NewRelic::Agent.logger.expects(:warn).once

    attributes = create_attributes
    100.times do |i|
      attributes.merge_custom_attributes(i.to_s => i)
    end

    assert_equal Attributes::COUNT_LIMIT, custom_attributes(attributes).length
  end

  def test_merge_untrusted_agent_attributes
    with_config(:'attributes.include' => 'request.parameters.*') do
      attributes = create_attributes
      params = {:foo => {:bar => 'baz'}}
      attributes.merge_untrusted_agent_attributes(params, 'request.parameters', AttributeFilter::DST_NONE)

      assert_equal({'request.parameters.foo.bar' => 'baz'}, agent_attributes(attributes))
    end
  end

  def test_merge_untrusted_agent_attributes_drops_long_keys
    with_config(:'attributes.include' => 'request.parameters.*') do
      attributes = create_attributes
      params = {
        'a' * 256 => 'too long',
        'foo' => 'bar'
      }
      attributes.merge_untrusted_agent_attributes(params, 'request.parameters', AttributeFilter::DST_NONE)

      assert_equal({'request.parameters.foo' => 'bar'}, agent_attributes(attributes))
    end
  end

  def test_merge_untrusted_agent_attributes_disallowed_in_high_security
    with_config(:high_security => true, :'attributes.include' => 'request.parameters.*') do
      attributes = create_attributes
      params = {'sneaky' => 'code'}

      attributes.merge_untrusted_agent_attributes('request.parameters', params, AttributeFilter::DST_NONE)

      assert_empty agent_attributes(attributes)
    end
  end

  def create_attributes
    filter = NewRelic::Agent::AttributeFilter.new(NewRelic::Agent.config)
    NewRelic::Agent::Attributes.new(filter)
  end

  def custom_attributes(attributes)
    attributes.custom_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
  end

  def assert_custom_attributes_empty(attributes)
    assert_empty custom_attributes(attributes)
  end

  def agent_attributes(attributes)
    attributes.agent_attributes_for(AttributeFilter::DST_TRANSACTION_TRACER)
  end
end
