def define_transaction_metadata_matcher_for(matcher_key, value_key = matcher_key)
  value_key = value_key.to_s

  RSpec::Matchers.define "have_#{matcher_key}" do |expected_value|
    match(:notify_expectation_failures => true) do |transaction|
      actual_value = transaction.to_h[value_key]
      if expected_value
        expect(actual_value).to eq(expected_value)
      else
        expect(actual_value).to_not be_nil
      end
    end

    match_when_negated(:notify_expectation_failures => true) do |transaction|
      actual_value = transaction.to_h[value_key]
      if expected_value
        expect(actual_value).to_not eq(expected_value)
      else
        expect(actual_value).to be_nil
      end
    end
  end
end

define_transaction_metadata_matcher_for(:id)
define_transaction_metadata_matcher_for(:namespace)
define_transaction_metadata_matcher_for(:action)

def define_transaction_sample_matcher_for(matcher_key, value_key = matcher_key)
  value_key = value_key.to_s

  RSpec::Matchers.define "include_#{matcher_key}" do |expected_value|
    match(:notify_expectation_failures => true) do |transaction|
      sample_data = transaction.to_h.dig("sample_data", value_key) || {}

      if expected_value
        expected_value = hash_including(expected_value) if expected_value.is_a?(Hash)
        expect(sample_data).to match(expected_value)
      else
        expect(sample_data).to be_present
      end
    end

    match_when_negated(:notify_expectation_failures => true) do |transaction|
      sample_data = transaction.to_h.dig("sample_data", value_key) || {}

      if expected_value
        expect(sample_data).to_not include(expected_value)
      else
        expect(sample_data).to be_empty
      end
    end
  end
end

define_transaction_sample_matcher_for(:sample_metadata, :metadata)
define_transaction_sample_matcher_for(:params)
define_transaction_sample_matcher_for(:environment)
define_transaction_sample_matcher_for(:session_data)
define_transaction_sample_matcher_for(:tags)
define_transaction_sample_matcher_for(:custom_data)
define_transaction_sample_matcher_for(:error_causes)

RSpec::Matchers.define :be_completed do
  match(:notify_expectation_failures => true) do |transaction|
    values_match? transaction.ext._completed?, true
  end
end

RSpec::Matchers.define :have_error do |error_class, error_message, error_backtrace|
  match(:notify_expectation_failures => true) do |transaction|
    transaction_error = transaction.to_h["error"]
    if error_class && error_message
      expect(transaction_error).to include(
        "name" => error_class,
        "message" => error_message,
        "backtrace" => error_backtrace ? JSON.dump(error_backtrace) : kind_of(String)
      )
    else
      expect(transaction_error).to be_any
    end
  end

  match_when_negated(:notify_expectation_failures => true) do |transaction|
    transaction_error = transaction.to_h["error"]
    if error_class && error_message
      expect(transaction_error).to_not include(
        "name" => error_class,
        "message" => error_message,
        "backtrace" => kind_of(String)
      )
    else
      expect(transaction_error).to be_nil
    end
  end
end

RSpec::Matchers.define :include_event do |event|
  match(:notify_expectation_failures => true) do |transaction|
    events = transaction.to_h["events"]
    if event
      expect(events).to include(format_event(event))
    else
      expect(events).to be_any
    end
  end

  match_when_negated(:notify_expectation_failures => true) do |transaction|
    events = transaction.to_h["events"]
    if event
      expect(events).to_not include(format_event(event))
    else
      expect(events).to be_empty
    end
  end

  def format_event(event)
    hash_including({
      "body" => "",
      "body_format" => Appsignal::EventFormatter::DEFAULT,
      "count" => 1,
      "name" => kind_of(String),
      "title" => ""
    }.merge(event.transform_keys(&:to_s)))
  end
end
RSpec::Matchers.alias_matcher :include_events, :include_event

RSpec::Matchers.define :include_metadata do |metadata|
  match(:notify_expectation_failures => true) do |transaction|
    actual_metadata = transaction.to_h["metadata"]
    if metadata
      expect(actual_metadata).to include(metadata)
    else
      expect(actual_metadata).to be_any
    end
  end

  match_when_negated(:notify_expectation_failures => true) do |transaction|
    actual_metadata = transaction.to_h["metadata"]
    if metadata
      expect(actual_metadata).to_not include(metadata)
    else
      expect(actual_metadata).to be_empty
    end
  end
end

RSpec::Matchers.define :include_breadcrumb do |action, category, message, metadata, time|
  match(:notify_expectation_failures => true) do |transaction|
    breadcrumbs = transaction.to_h.dig("sample_data", "breadcrumbs")
    if action
      breadcrumb = format_breadcrumb(action, category, message, metadata, time)
      expect(breadcrumbs).to include(breadcrumb)
    else
      expect(transaction.to_h.dig("sample_data", "breadcrumbs")).to be_any
    end
  end

  match_when_negated(:notify_expectation_failures => true) do |transaction|
    breadcrumbs = transaction.to_h.dig("sample_data", "breadcrumbs")
    if action
      breadcrumb = format_breadcrumb(action, category, message, metadata, time)
      expect(breadcrumbs).to_not include(breadcrumb)
    else
      expect(breadcrumbs).to_not be_any
    end
  end

  def format_breadcrumb(action, category, message, metadata, time)
    {
      "action" => action,
      "category" => category,
      "message" => message || "",
      "metadata" => metadata || {},
      "time" => time || kind_of(Integer)
    }
  end
end
RSpec::Matchers.alias_matcher :include_breadcrumbs, :include_breadcrumb

RSpec::Matchers.define :have_queue_start do |queue_start_time|
  match(:notify_expectation_failures => true) do |transaction|
    actual_start = transaction.ext.queue_start
    if queue_start_time
      expect(actual_start).to eq(queue_start_time)
    else
      expect(actual_start).to_not be_nil
    end
  end

  match_when_negated(:notify_expectation_failures => true) do |transaction|
    actual_start = transaction.ext.queue_start
    if queue_start_time
      expect(actual_start).to_not eq(queue_start_time)
    else
      expect(actual_start).to be_nil
    end
  end
end
