# 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

# https://newrelic.atlassian.net/browse/RUBY-747
require './app'
require 'fake_collector'
require_relative '../../../helpers/exceptions'

class ErrorController < ApplicationController
  include NewRelic::TestHelpers::Exceptions

  newrelic_ignore :only => :ignored_action

  def controller_error
    raise 'this is an uncaught controller error'
  end

  def exception_error
    raise Exception.new('wobble')
  end

  def view_error
    render(:inline => "<% raise 'this is an uncaught view error' %>")
  end

  def model_error
    Foo.new.raise_error
  end

  def ignored_action
    raise 'this error should not be noticed'
  end

  def ignored_error
    raise NewRelic::TestHelpers::Exceptions::IgnoredError.new('this error should not be noticed')
  end

  def ignored_status_code
    render(body: "Protip: you probably shouldn't ignore 500 errors", status: 500)
  end

  def server_ignored_error
    raise NewRelic::TestHelpers::Exceptions::ServerIgnoredError.new('this is a server ignored error')
  end

  def frozen_error
    e = RuntimeError.new('frozen errors make a refreshing treat on a hot summer day')
    e.freeze
    raise e
  end

  def string_noticed_error
    NewRelic::Agent.notice_error('trilobites died out millions of years ago')
    render(body: 'trilobites')
  end

  def noticed_error
    NewRelic::Agent.notice_error(RuntimeError.new('this error should be noticed'))
    render(body: 'Shoulda noticed an error')
  end

  def middleware_error
    render(body: 'everything went great')
  end

  def error_with_custom_params
    NewRelic::Agent.add_custom_attributes(:texture => 'chunky')
    raise 'bad things'
  end

  def noticed_error_with_trace_only
    NewRelic::Agent.notice_error('Raise the gates!', :trace_only => true)
    render(body: 'Runner 5')
  end

  def noticed_error_with_expected_error
    NewRelic::Agent.notice_error('Raise the gates!', :expected => true)
    render(body: 'Runner 5')
  end
end

class ErrorsWithoutSSCTest < ActionDispatch::IntegrationTest
  extend Multiverse::Color

  include MultiverseHelpers

  setup_and_teardown_agent do |collector|
    setup_collector_mocks
    @error_collector = agent.error_collector
  end

  # Let base class override this without moving where we start the agent
  def setup_collector_mocks
    $collector.stub('connect', {'agent_run_id' => 666}, 200)
  end

  def last_error
    errors.last
  end

  def test_error_collector_should_be_enabled
    assert NewRelic::Agent.config[:agent_enabled]
    assert NewRelic::Agent.config[:'error_collector.enabled']
    assert_predicate @error_collector, :enabled?
  end

  def test_should_capture_routing_error
    get('/bad_route')

    assert_error_reported_once('this is an uncaught routing error', nil, nil)
  end

  def test_should_capture_errors_raised_in_middleware_before_call
    get('/error/middleware_error/before')

    assert_error_reported_once('middleware error', nil, nil)
  end

  def test_should_capture_errors_raised_in_middleware_after_call
    get('/error/middleware_error/after')

    assert_error_reported_once('middleware error', nil, nil)
  end

  def test_should_capture_request_uri_and_params
    get('/error/controller_error?eat=static')

    assert_equal('/error/controller_error', attributes_for_single_error_posted('agentAttributes')['request.uri'])

    expected_params = {
      'request.parameters.eat' => 'static',
      'http.statusCode' => 500
    }
    attributes = agent_attributes_for_single_error_posted

    expected_params.each do |key, value|
      assert_equal value, attributes[key]
    end
  end

  def test_should_capture_error_raised_in_view
    get('/error/view_error')

    assert_equal 1, errors.size
    assert_includes ['this is an uncaught view error', 'ActionView::Template::Error'], errors[0].message
  end

  def test_should_capture_error_raised_in_controller
    get('/error/controller_error')

    assert_error_reported_once('this is an uncaught controller error',
      'Controller/error/controller_error')
  end

  def test_should_capture_error_raised_in_model
    get('/error/model_error')

    assert_error_reported_once('this is an uncaught model error',
      'Controller/error/model_error')
  end

  def test_should_capture_noticed_error_in_controller
    get('/error/noticed_error')

    assert_error_reported_once('this error should be noticed',
      'Controller/error/noticed_error')
  end

  def test_should_capture_frozen_errors
    get('/error/frozen_error')

    assert_error_reported_once('frozen errors make a refreshing treat on a hot summer day',
      'Controller/error/frozen_error')
  end

  def test_should_capture_string_noticed_errors
    get('/error/string_noticed_error')

    assert_error_reported_once('trilobites died out millions of years ago',
      'Controller/error/string_noticed_error')
  end

  # Important choice of controller_error, since this goes through both the
  # transaction and the rack error collector, so risks multiple counting!
  def test_should_capture_multiple_errors
    40.times do
      get('/error/controller_error')
    end

    assert_errors_reported('this is an uncaught controller error',
      NewRelic::Agent::ErrorCollector::MAX_ERROR_QUEUE_LENGTH,
      40, nil, 40)
  end

  def test_should_capture_manually_noticed_error
    NewRelic::Agent.notice_error(RuntimeError.new('this is a noticed error'))

    assert_error_reported_once('this is a noticed error', nil, nil)
  end

  def test_should_apply_parameter_filtering
    get('/error/controller_error?secret=shouldnotbecaptured&other=whatever')
    attributes = agent_attributes_for_single_error_posted

    assert_equal('[FILTERED]', attributes['request.parameters.secret'])
    assert_equal('whatever', attributes['request.parameters.other'])
  end

  def test_should_apply_parameter_filtering_for_non_standard_errors
    get('/error/exception_error?secret=shouldnotbecaptured&other=whatever')
    attributes = agent_attributes_for_single_error_posted

    assert_equal('[FILTERED]', attributes['request.parameters.secret'])
    assert_equal('whatever', attributes['request.parameters.other'])
  end

  def test_should_not_notice_errors_from_ignored_action
    get('/error/ignored_action')

    assert_empty(errors,
      'Noticed an error that should have been ignored')
  end

  def test_should_not_notice_ignored_error_classes
    get('/error/ignored_error')

    assert_empty(errors,
      'Noticed an error that should have been ignored')
  end

  def test_should_not_fail_apdex_for_ignored_error_class_noticed
    get('/error/ignored_error')

    assert_metrics_recorded({
      'Apdex' => {:apdex_f => 0},
      'Apdex/error/ignored_error' => {:apdex_f => 0}
    })
  end

  def test_should_not_notice_filtered_errors
    filter = proc do |error|
      !error.kind_of?(RuntimeError)
    end

    with_ignore_error_filter(filter) do
      get('/error/controller_error')
    end

    assert_empty(errors,
      'Noticed an error that should have been ignored')
  end

  def test_should_not_notice_ignored_status_codes
    with_config(:'error_collector.ignore_status_codes' => '500') do
      get('/error/ignored_status_code')

      assert_empty(errors, 'Noticed an error that should have been ignored')
    end
  end

  def test_should_notice_server_ignored_error_if_no_server_side_config
    get('/error/server_ignored_error')

    assert_error_reported_once('this is a server ignored error')
  end

  def test_captured_errors_should_include_custom_params
    with_config(:'error_collector.attributes.enabled' => true) do
      get('/error/error_with_custom_params')

      assert_error_reported_once('bad things')

      attributes = user_attributes_for_single_error_posted

      assert_equal({'texture' => 'chunky'}, attributes)
    end
  end

  def test_captured_errors_should_include_custom_params_with_legacy_setting
    with_config(:'error_collector.capture_attributes' => true) do
      get('/error/error_with_custom_params')

      assert_error_reported_once('bad things')

      attributes = user_attributes_for_single_error_posted

      assert_equal({'texture' => 'chunky'}, attributes)
    end
  end

  def test_captured_errors_should_not_include_custom_params_if_config_says_no
    # TODO: stop the flapping/flaking
    skip 'Flaps too often with JRuby' if defined?(JRuby)

    with_config(:'error_collector.attributes.enabled' => false) do
      get('/error/error_with_custom_params')

      assert_error_reported_once('bad things')

      attributes = user_attributes_for_single_error_posted

      assert_empty attributes
    end
  end

  def test_should_not_increment_metrics_on_expected_error_errors
    get('/error/noticed_error_with_expected_error')

    assert_equal(1, errors.size,
      'Error with :expected should have been recorded')

    assert_metrics_not_recorded([
      'Errors/all',
      'Errors/Controller/error/noticed_error_with_expected_error'
    ])

    assert_metrics_recorded('Apdex' => {:apdex_s => 1})
    assert_metrics_recorded(['ErrorsExpected/all'])
  end

  protected

  def errors
    @error_collector.error_trace_aggregator.instance_variable_get(:@errors)
  end

  def errors_with_message(message)
    errors.select { |error| error.message == message }
  end

  def assert_errors_reported(message, queued_count, total_count = queued_count, txn_name = nil, apdex_f = 1)
    expected = {:call_count => total_count}

    assert_metrics_recorded('Errors/all' => expected)
    assert_metrics_recorded("Errors/#{txn_name}" => expected) if txn_name

    unless apdex_f.nil?
      assert_metrics_recorded('Apdex' => {:apdex_f => apdex_f})
    end

    assert_equal(queued_count,
      errors_with_message(message).size,
      "Wrong number of errors with message #{message.inspect} found")
  end

  def assert_error_reported_once(message, txn_name = nil, apdex_f = 1)
    assert_errors_reported(message, 1, 1, txn_name, apdex_f)
  end
end

class ErrorsWithSSCTest < ErrorsWithoutSSCTest
  def setup_collector_mocks
    $collector.stub('connect', {
      'agent_run_id' => 1,
      'agent_config' => {
        'error_collector.ignore_classes' => ['NewRelic::TestHelpers::Exceptions::IgnoredError', 'NewRelic::TestHelpers::Exceptions::ServerIgnoredError'],
        'error_collector.enabled' => true
      },
      'collect_errors' => true
    }, 200)
  end

  def test_should_notice_server_ignored_error_if_no_server_side_config
    # Overrides test in base class, since doesn't apply
  end

  def test_should_ignore_server_ignored_errors
    # TODO: stop the flapping/flaking
    skip 'Flaps too often with JRuby' if defined?(JRuby)

    get('/error/server_ignored_error')

    assert_empty(errors,
      'Noticed an error that should have been ignored' + errors.join(', '))
  end
end
