# vim: syntax=ruby:expandtab:shiftwidth=2:softtabstop=2:tabstop=2
#
# Copyright (c) 2016-present, Facebook, Inc.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

require 'chefspec'
require 'chefspec/lib/chefspec/matchers/render_file_matcher.rb'

$VERBOSE = nil

module FB
  class Spec
    def self.fbspec_init(cookbook_path, platforms)
      RSpec.configure do |config|
        config.expect_with :rspec do |c|
          c.syntax = [:should, :expect]
        end
        config.mock_with :rspec do |c|
          c.syntax = [:should, :expect]
        end
        config.cookbook_path = cookbook_path
        if ENV['FB_RSPEC_PROFILING']
          require 'ruby-prof'
          config.before(:example) do
            RubyProf.start
          end
          config.after(:example) do
            result = RubyProf.stop
            printer = RubyProf::GraphPrinter.new(result)
            profile_name = "rspec_profile-#{DateTime.now.iso8601(4)}.out"
            File.open(profile_name, 'w+') do |file|
              printer.print(file)
            end
            puts "Rspec profiling dumped to #{profile_name}"
          end
        end
      end
      FB::Spec.configure do |config|
        config.default_platforms = platforms['default']
        config.extra_platforms = platforms['extra']
        config.all_platforms = platforms['default'].merge(platforms['extra'])
      end
    end

    class << self
      attr_accessor :configuration
    end

    def self.configure
      self.configuration ||= Configuration.new
      yield(configuration)
    end

    class Configuration
      attr_accessor :default_platforms, :extra_platforms, :all_platforms

      def initialize
        @default_platforms = {}
        @extra_platforms = {}
        @all_platforms = {}
      end
    end

    class Testcase
      include RSpec::Matchers
      def initialize(config)
        @config = config
      end

      # Local sites should monkeypatch this method if necessary
      def chef_run_block_extras(_node); end

      def chef_run(**extra_args)
        extra_args[:step_into] ||= []
        extra_args[:step_into] << 'whyrun_safe_ruby_block'
        extra_args[:step_into].uniq!
        runner_args = {
          :platform => @config['platform'],
          :version => @config['version'],
          :os_name => 'xxxx',
        }.merge(extra_args)
        ChefSpec::SoloRunner.new(runner_args) do |node|
          chef_run_block_extras(node)
          yield(node) if block_given?
        end
      end

      def fixture_path(path, name)
        File.join(
          File.dirname(caller(2..2).first.split(':').first),
          'fixtures', path, name
        )
      end

      def platform
        @config[:os_name]
      end

      def fixture(name)
        # look in fb_cookbooks/spec/fixtures/centos6/
        profile_path = fixture_path(
          self.platform.to_s, name
        )
        # look in fb_cookbook/spec/fixtures/default/ (fallback)
        default_path = fixture_path('default', name)

        path = File.exist?(profile_path) ? profile_path : default_path
        begin
          File.read(path)
        rescue StandardError
          puts "Fixture #{name} not found " +
            "(tried: #{profile_path}, #{default_path})"
          raise
        end
      end
    end

    class Runner
      def initialize(config, &block)
        RSpec.describe config[:described_recipe] do
          config[:supported].each do |platform|
            FB::Spec.configuration.all_platforms[platform].each do |os|
              os[:os_name] = platform
              tags = config[:supported].map do |x|
                { x => false }
              end.reduce({}, :merge)
              tags[platform] = true
              config[:xxx] = 1
              context "#{platform} (#{os['version']})", tags do
                instance_exec(
                  FB::Spec::Testcase.new(os),
                  &block
                )
              end
            end
          end
        end
      end
    end
  end
end

def recipe(name, options = [], &block)
  if options.include?(:supported)
    supported = [*options[:supported]].map(&:to_sym)
  else
    supported = FB::Spec.configuration.default_platforms.keys
  end

  if options.include?(:unsupported)
    supported -= [*options[:unsupported]].map(&:to_sym)
  end

  FB::Spec::Runner.new(
    { :supported => supported, :described_recipe => name },
    &block
  )
end
