require 'vcr'

module VCRHelpers

  def normalize_cassette_hash(cassette_hash)
    cassette_hash['recorded_with'] = "VCR #{VCR.version}"
    cassette_hash['http_interactions'].map! { |h| normalize_http_interaction(h) }
    cassette_hash
  end

  def normalize_headers(object)
    object.headers = {} and return if object.headers.nil?
    object.headers = {}.tap do |hash|
      object.headers.each do |key, value|
        hash[key.downcase] = value
      end
    end
  end

  def static_timestamp
    @static_timestamp ||= Time.now
  end

  def normalize_http_interaction(hash)
    VCR::HTTPInteraction.from_hash(hash).tap do |i|
      normalize_headers(i.request)
      normalize_headers(i.response)

      i.recorded_at &&= static_timestamp
      i.request.body ||= ''
      i.response.body ||= ''
      i.response.status.message ||= ''
      i.response.adapter_metadata.clear

      # Remove non-deterministic headers and headers
      # that get added by a particular HTTP library (but not by others)
      i.response.headers.reject! { |k, v| %w[ server date connection ].include?(k) }
      i.request.headers.reject! { |k, v| %w[ accept user-agent connection expect date ].include?(k) }

      # Some HTTP libraries include an extra space ("OK " instead of "OK")
      i.response.status.message = i.response.status.message.strip

      if @http_lib_config.to_s =~ /excon|faraday/
        # Excon/Faraday do not expose the status message or http version,
        # so we have no way to record these attributes.
        i.response.status.message = nil
        i.response.http_version = nil
      elsif @http_lib_config.to_s.include?('webmock')
        # WebMock does not expose the HTTP version so we have no way to record it
        i.response.http_version = nil
      end
    end
  end

  def normalize_cassette_content(content)
    return content unless @http_lib_config.to_s.include?('patron')
    cassette_hash = YAML.load(content)
    cassette_hash['http_interactions'].map! do |hash|
      VCR::HTTPInteraction.from_hash(hash).tap do |i|
        i.request.headers = (i.request.headers || {}).merge!('Expect' => [''])
      end.to_hash
    end
    YAML.dump(cassette_hash)
  end

  def modify_file(file_name, orig_text, new_text)
    cd('.') do
      file = File.read(file_name)
      regex = /#{Regexp.escape(orig_text)}/
      expect(file).to match(regex)

      file = file.gsub(regex, new_text)
      File.open(file_name, 'w') { |f| f.write(file) }
    end
  end
end
World(VCRHelpers)

Given(/the following files do not exist:/) do |files|
  check_file_presence(files.raw.map{|file_row| file_row[0]}, false)
end

Given(/^a previously recorded cassette file "([^"]*)" with:$/) do |file_name, content|
  write_file(file_name, normalize_cassette_content(content))
end

Given(/^it is (.*)$/) do |date_string|
  set_environment_variable('DATE_STRING', date_string)
end

Given(/^that port numbers in "([^"]*)" are normalized to "([^"]*)"$/) do |file_name, port|
  cd('.') do
    contents = File.read(file_name)
    contents = contents.gsub(/:\d{2,}\//, ":#{port}/")
    File.open(file_name, 'w') { |f| f.write(contents) }
  end
end

Given(/^I am configuring with "([^"]*)" using "([^"]*)"$/) do |configuration, adapter_name|
  @http_lib_config = [ configuration, adapter_name ]
end

When(/^I modify the file "([^"]*)" to replace "([^"]*)" with "([^"]*)"$/) do |file_name, orig_text, new_text|
  modify_file(file_name, orig_text, new_text)
end

When(/^I append to file "([^"]*)":$/) do |file_name, content|
  append_to_file(file_name, "\n" + content)
end

When(/^I set the "([^"]*)" environment variable to "([^"]*)"$/) do |var, value|
  set_environment_variable(var, value)
end

Then(/^it should (pass|fail) with an error like:$/) do |pass_fail, partial_output|
  if pass_fail == 'pass'
    expect(last_command_started).to be_successfully_executed
  else
    expect(last_command_started).not_to be_successfully_executed
  end

  # different implementations place the exception class at different
  # places relative to the message (i.e. with a multiline error message)
  process_output = all_output.gsub(/\s*\(VCR::Errors::\w+\)/, '')

  # Some implementations include extra leading spaces, for some reason...
  process_output.gsub!(/^\s*/, '')
  partial_output.gsub!(/^\s*/, '')

  expect(process_output).to include_output_string partial_output
end

Then(/^the output should contain each of the following:$/) do |table|
  table.raw.flatten.each do |string|
    expect(all_output).to include_output_string string
  end
end

Then(/^the file "([^"]*)" should contain YAML like:$/) do |file_name, expected_content|
  actual_content = cd('.') { File.read(file_name) }
  expect(normalize_cassette_hash(YAML.load(actual_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s)))
end

Then(/^the file "([^"]*)" should contain JSON like:$/) do |file_name, expected_content|
  actual_content = cd('.') { File.read(file_name) }
  actual = JSON.parse(actual_content)
  expected = JSON.parse(expected_content.to_s)
  expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected))
end

Then(/^the file "([^"]*)" should contain compressed YAML like:$/) do |file_name, expected_content|
  actual_content = cd('.') { File.read(file_name) }
  unzipped_content = Zlib::Inflate.inflate(actual_content)
  expect(normalize_cassette_hash(YAML.load(unzipped_content))).to eq(normalize_cassette_hash(YAML.load(expected_content.to_s)))
end

Then(/^the file "([^"]*)" should contain ruby like:$/) do |file_name, expected_content|
  actual_content = cd('.') { File.read(file_name) }
  actual = eval(actual_content)
  expected = eval(expected_content)
  expect(normalize_cassette_hash(actual)).to eq(normalize_cassette_hash(expected))
end

Then(/^the file "([^"]*)" should contain each of these:$/) do |file_name, table|
  table.raw.flatten.each do |string|
    expect(file_name).to have_file_content(/#{string}/)
  end
end

Then(/^the file "([^"]*)" should contain a YAML fragment like:$/) do |file_name, fragment|
  cd('.') do
    file_content = File.read(file_name)

    # Normalize by removing leading and trailing whitespace...
    file_content = file_content.split("\n").map do |line|
      # Different versions of psych use single vs. double quotes
      # And then 2.1 sometimes adds quotes...
      line.strip.gsub('"', "'").gsub("'", '')
    end.join("\n")

    expect(file_content).to include(fragment.gsub("'", ''))
  end
end

Then(/^the cassette "([^"]*)" should have the following response bodies:$/) do |file, table|
  interactions = cd('.') { YAML.load_file(file) }['http_interactions'].map { |h| VCR::HTTPInteraction.from_hash(h) }
  actual_response_bodies = interactions.map { |i| i.response.body }
  expected_response_bodies = table.raw.flatten
  expect(actual_response_bodies).to match(expected_response_bodies)
end

Then(/^it should (pass|fail)$/) do |pass_fail|
  if pass_fail == 'pass'
    expect(last_command_started).to be_successfully_executed
  else
    expect(last_command_started).not_to be_successfully_executed
  end
end
