# 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/transaction/trace'
require 'new_relic/agent/database'

class NewRelic::Agent::Transaction::TraceTest < Minitest::Test
  def setup
    nr_freeze_process_time
    @start_time = Process.clock_gettime(Process::CLOCK_REALTIME)
    @trace = NewRelic::Agent::Transaction::Trace.new(@start_time)
    @trace.root_node.end_trace(@start_time)

    NewRelic::Agent.instance.refresh_attribute_filter
    filter = NewRelic::Agent.instance.attribute_filter
    @fake_attributes = NewRelic::Agent::Attributes.new(filter)

    @trace.attributes = @fake_attributes
  end

  def test_start_time
    assert_equal @start_time, @trace.start_time
  end

  def test_node_count
    assert_equal 0, @trace.node_count
  end

  def test_sample_id_is_the_object_id
    assert_equal @trace.object_id, @trace.sample_id
  end

  def test_create_node_increases_node_count
    @trace.create_node(0.0, 'foo')

    assert_equal 1, @trace.node_count
  end

  def test_duration_is_the_root_node_duration
    assert_equal @trace.duration, @trace.root_node.duration
  end

  def test_create_node
    result = @trace.create_node(0.0, 'goo')

    assert_in_delta(0.0, result.entry_timestamp)
    assert_equal 'goo', result.metric_name
  end

  def test_each_node_delegates_to_root_node
    block = proc {}
    @trace.root_node.expects(:each_node)
    @trace.each_node(&block)
  end

  def test_collector_array_contains_start_time
    expected = NewRelic::Helper.time_to_millis(@start_time)

    assert_collector_array_contains(:start_time, expected)
  end

  def test_root_node
    assert_in_delta(0.0, @trace.root_node.entry_timestamp)
    assert_equal 'ROOT', @trace.root_node.metric_name
  end

  def test_prepare_to_send_returns_self
    result = @trace.prepare_to_send!

    assert_equal @trace, result
  end

  def test_prepare_to_send_returns_self_if_already_prepared
    @trace.prepare_to_send!

    result = @trace.prepare_to_send!

    assert_equal @trace, result
  end

  def test_prepare_to_send_is_idempotent
    NewRelic::Agent::Database.stubs(:should_record_sql?).returns(true)
    @trace.expects(:collect_explain_plans!).once
    @trace.expects(:prepare_sql_for_transmission!).once
    @trace.prepare_to_send!
    @trace.prepare_to_send!
  end

  def test_prepare_to_send_handles_sql_and_does_not_strip_if_should_record_sql_is_true
    NewRelic::Agent::Database.stubs(:should_record_sql?).returns(true)
    @trace.expects(:collect_explain_plans!).once
    @trace.expects(:prepare_sql_for_transmission!).once
    @trace.expects(:strip_sql!).never
    @trace.prepare_to_send!
  end

  def test_prepare_to_send_strips_and_does_not_handle_sql_if_should_record_sql_is_false
    NewRelic::Agent::Database.stubs(:should_record_sql?).returns(false)
    @trace.expects(:collect_explain_plans!).never
    @trace.expects(:prepare_sql_for_transmission!).never
    @trace.expects(:strip_sql!).once
    @trace.prepare_to_send!
  end

  def test_prepare_to_send_collects_explain_plans
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = NewRelic::Agent::Database::Statement.new('select * from pelicans')

    @trace.root_node.children << node

    with_config(:'transaction_tracer.explain_threshold' => 1) do
      @trace.prepare_to_send!
    end

    assert node[:explain_plan]
  end

  def test_prepare_to_send_prepares_sql_for_transmission
    NewRelic::Agent::Database.stubs(:record_sql_method).returns(:obfuscated)

    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node[:sql] = NewRelic::Agent::Database::Statement.new("select * from pelicans where name = '1337807';")
    @trace.root_node.children << node

    @trace.prepare_to_send!

    assert_equal 'select * from pelicans where name = ?;', node[:sql]
  end

  def test_prepare_to_send_strips_sql
    NewRelic::Agent::Database.stubs(:should_record_sql?).returns(false)
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = NewRelic::Agent::Database::Statement.new('select * from pelicans;')

    @trace.root_node.children << node
    @trace.prepare_to_send!

    refute node[:sql]
  end

  def test_collect_explain_plans!
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = ''

    @trace.root_node.children << node

    with_config(:'transaction_tracer.explain_threshold' => 1) do
      @trace.collect_explain_plans!
    end

    assert node[:explain_plan]
  end

  def test_collect_explain_plans_does_not_attach_explain_plans_if_duration_is_too_short
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(1)
    node.stubs(:explain_sql).returns('')
    node[:sql] = ''

    @trace.root_node.children << node

    with_config(:'transaction_tracer.explain_threshold' => 2) do
      @trace.collect_explain_plans!
    end

    refute node[:explain_plan]
  end

  def test_collect_explain_plans_does_not_attach_explain_plans_without_sql
    node = @trace.create_node(0.0, 'nope_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = nil

    @trace.root_node.children << node

    with_config(:'transaction_tracer.explain_threshold' => 1) do
      @trace.collect_explain_plans!
    end

    refute node[:explain_plan]
  end

  def test_collect_explain_plans_does_not_attach_explain_plans_if_db_says_not_to
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = ''

    NewRelic::Agent::Database.stubs(:should_collect_explain_plans?).returns(false)

    @trace.root_node.children << node

    with_config(:'transaction_tracer.explain_threshold' => 1) do
      @trace.collect_explain_plans!
    end

    refute node[:explain_plan]
  end

  def test_prepare_sql_for_transmission_obfuscates_sql_if_record_sql_method_is_obfuscated
    NewRelic::Agent::Database.stubs(:record_sql_method).returns(:obfuscated)

    node = @trace.create_node(0.0, 'has_sql')
    node[:sql] = NewRelic::Agent::Database::Statement.new("select * from pelicans where name = '1337807';")
    @trace.root_node.children << node

    @trace.prepare_sql_for_transmission!

    assert_equal 'select * from pelicans where name = ?;', node[:sql]
  end

  def test_prepare_sql_for_transmission_does_not_modify_sql_if_record_sql_method_is_raw
    NewRelic::Agent::Database.stubs(:record_sql_method).returns(:raw)

    node = @trace.create_node(0.0, 'has_sql')
    node[:sql] = NewRelic::Agent::Database::Statement.new("select * from pelicans where name = '1337807';")
    @trace.root_node.children << node

    @trace.prepare_sql_for_transmission!

    assert_equal "select * from pelicans where name = '1337807';", node[:sql]
  end

  def test_prepare_sql_for_transmission_removes_sql_if_record_sql_method_is_off
    NewRelic::Agent::Database.stubs(:record_sql_method).returns(:off)

    node = @trace.create_node(0.0, 'has_sql')
    node[:sql] = NewRelic::Agent::Database::Statement.new("select * from pelicans where name = '1337807';")
    @trace.root_node.children << node

    @trace.prepare_sql_for_transmission!

    refute node[:sql]
  end

  def test_strip_sql!
    node = @trace.create_node(0.0, 'has_sql')
    node.stubs(:duration).returns(2)
    node.stubs(:explain_sql).returns('')
    node[:sql] = NewRelic::Agent::Database::Statement.new('select * from pelicans;')

    @trace.root_node.children << node
    @trace.strip_sql!

    refute node[:sql]
  end

  def test_collector_array_contains_root_node_duration
    @trace.root_node.end_trace(1)

    assert_collector_array_contains(:duration, 1000)
  end

  def test_collector_array_contains_transaction_name
    @trace.transaction_name = 'zork'

    assert_collector_array_contains(:transaction_name, 'zork')
  end

  def test_transaction_name_gets_coerced_into_a_string
    @trace.transaction_name = 1337807

    assert_collector_array_contains(:transaction_name, '1337807')
  end

  def test_collector_array_contains_uri
    @fake_attributes.add_agent_attribute(:'request.uri',
      'http://windows95tips.com/',
      NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER)

    assert_collector_array_contains(:uri, 'http://windows95tips.com/')
  end

  def test_uri_gets_coerced_into_a_string
    @fake_attributes.add_agent_attribute(:'request.uri',
      95,
      NewRelic::Agent::AttributeFilter::DST_TRANSACTION_TRACER)

    assert_collector_array_contains(:uri, '95')
  end

  def test_collector_array_contains_trace_tree
    assert_collector_array_contains(:trace_tree, extract_trace_tree(@trace))
  end

  def test_collector_array_contains_guid
    @trace.guid = 'DEADBEEF8BADF00D'

    assert_collector_array_contains(:guid, 'DEADBEEF8BADF00D')
  end

  def test_guid_gets_coerced_into_a_string
    @trace.guid = 42

    assert_collector_array_contains(:guid, '42')
  end

  def test_collector_array_contains_nil_for_reserved
    assert_collector_array_contains(:reserved, nil)
  end

  def test_collector_array_contains_synthetics_resource_id
    @fake_attributes.add_intrinsic_attribute(:synthetics_resource_id, '31415926')

    assert_collector_array_contains(:synthetics_resource_id, '31415926')
  end

  def test_to_collector_array_encodes_trace_tree_with_given_encoder
    fake_encoder = mock
    fake_encoder.expects(:encode).with(extract_trace_tree(@trace))
    @trace.to_collector_array(fake_encoder)
  end

  def test_synthetics_resource_id_gets_coerced_to_a_string
    @fake_attributes.add_intrinsic_attribute(:synthetics_resource_id, 31415926)

    assert_collector_array_contains(:synthetics_resource_id, '31415926')
  end

  def test_trace_tree_coerces_start_time_to_a_float
    assert_kind_of Float, extract_trace_tree(@trace).first
  end

  def test_trace_tree_includes_start_time
    assert_trace_tree_contains(:start_time, @start_time)
  end

  def test_trace_tree_includes_unused_legacy_request_params
    assert_trace_tree_contains(:unused_legacy_request_params, {})
  end

  def test_trace_tree_includes_unused_legacy_custom_params
    assert_trace_tree_contains(:unused_legacy_custom_params, {})
  end

  def test_trace_tree_contains_serialized_root_node
    assert_trace_tree_contains(:root_node, @trace.root_node.to_array)
  end

  def test_trace_tree_contains_attributes
    @fake_attributes.add_agent_attribute(:foo, 'bar', NewRelic::Agent::AttributeFilter::DST_ALL)
    @fake_attributes.add_intrinsic_attribute(:foo, 'bar')
    @fake_attributes.merge_custom_attributes(:foo => 'bar')

    expected = {
      'agentAttributes' => {:foo => 'bar'},
      'userAttributes' => {'foo' => 'bar'},
      'intrinsics' => {:foo => 'bar'}
    }

    assert_trace_tree_contains(:attributes, expected)
  end

  def extract_trace_tree(trace)
    trace.to_collector_array(NewRelic::Agent::NewRelicService::Encoders::Identity)[4]
  end

  def assert_trace_tree_contains(key, expected)
    indices = [
      :start_time,
      :unused_legacy_request_params,
      :unused_legacy_custom_params,
      :root_node,
      :attributes
    ]

    tree = extract_trace_tree(@trace)

    assert_equal expected, tree[indices.index(key)]
  end

  def assert_collector_array_contains(key, expected)
    indices = [
      :start_time,
      :duration,
      :transaction_name,
      :uri,
      :trace_tree,
      :guid,
      :reserved,
      :forced?,
      :xray_session_id,
      :synthetics_resource_id
    ]

    encoder = NewRelic::Agent::NewRelicService::Encoders::Identity
    if expected.nil?
      assert_nil @trace.to_collector_array(encoder)[indices.index(key)]
    else
      assert_equal expected, @trace.to_collector_array(encoder)[indices.index(key)]
    end
  end
end
