require 'yaml'

module Minitest
  module Reporters

    class MeanTimeReporter < DefaultReporter

      # @param options [Hash]
      # @option previous_runs_filename [String] Contains the times for each test
      #   by description. Defaults to '/tmp/minitest_reporters_previous_run'.
      # @option report_filename [String] Contains the parsed results for the
      #   last test run. Defaults to '/tmp/minitest_reporters_report'.
      # @return [Minitest::Reporters::MeanTimeReporter]
      def initialize(options = {})
        super

        @all_suite_times = []
      end

      # Copies the suite times from the
      # {Minitest::Reporters::DefaultReporter#after_suite} method, making them
      # available to this class.
      #
      # @return [Hash<String => Float>]
      def after_suite(suite)
        super

        @all_suite_times = @suite_times
      end

      # Runs the {Minitest::Reporters::DefaultReporter#report} method and then
      # enhances it by storing the results to the 'previous_runs_filename' and
      # outputs the parsed results to both the 'report_filename' and the
      # terminal.
      #
      def report
        super

        create_or_update_previous_runs!

        create_new_report!
      end

      protected

      attr_accessor :all_suite_times

      private

      # @return [Hash<String => Float>]
      def current_run
        Hash[all_suite_times]
      end

      # @return [Hash] Sets default values for the filenames used by this class.
      def defaults
        {
          previous_runs_filename: '/tmp/minitest_reporters_previous_run',
          report_filename:        '/tmp/minitest_reporters_report',
        }
      end

      # Added to the top of the report file to be helpful.
      #
      # @return [String]
      def report_header
        "Samples: #{samples}\n\n"
      end

      # The report itself. Displays statistic about all runs, ideal for use with
      # the Unix 'head' command. Listed in slowest average descending order.
      #
      # @return [String]
      def report_body
        previous_run.each_with_object([]) do |(description, timings), obj|
          size = timings.size
          sum  = timings.inject { |total, x| total + x }
          avg  = (sum / size).round(9).to_s.ljust(12)
          min  = timings.min.to_s.ljust(12)
          max  = timings.max.to_s.ljust(12)

          obj << "#{avg_label} #{avg} " \
                 "#{min_label} #{min} " \
                 "#{max_label} #{max} " \
                 "#{des_label} #{description}\n"
        end.sort.reverse.join
      end

      # @return [Hash]
      def options
        defaults.merge!(@options)
      end

      # @return [Hash<String => Array<Float>]
      def previous_run
        @previous_run ||= YAML.load_file(previous_runs_filename)
      end

      # @return [String] The path to the file which contains all the durations
      #   for each test run. The previous runs file is in YAML format, using the
      #   test name for the key and an array containing the time taken to run
      #   this test for values.
      #
      # @return [String]
      def previous_runs_filename
        options[:previous_runs_filename]
      end

      # Returns a boolean indicating whether a previous runs file exists.
      #
      # @return [Boolean]
      def previously_ran?
        File.exist?(previous_runs_filename)
      end

      # @return [String] The path to the file which contains the parsed test
      #   results. The results file contains a line for each test with the
      #   average time of the test, the minimum time the test took to run,
      #   the maximum time the test took to run and a description of the test
      #   (which is the test name as emitted by Minitest).
      def report_filename
        options[:report_filename]
      end

      # A barbaric way to find out how many runs are in the previous runs file;
      # this method takes the first test listed, and counts its samples
      # trusting (naively) all runs to be the same number of samples. This will
      # produce incorrect averages when new tests are added, so it is advised
      # to restart the statistics by removing the 'previous runs' file.
      #
      # @return [Fixnum]
      def samples
        return 1 unless previous_run.first[1].is_a?(Array)

        previous_run.first[1].size
      end

      # Creates a new 'previous runs' file, or updates the existing one with
      # the latest timings.
      #
      # @return [void]
      def create_or_update_previous_runs!
        if previously_ran?
          current_run.each do |description, elapsed|
          new_times = if previous_run["#{description}"]
                        Array(previous_run["#{description}"]) << elapsed

                      else
                        Array(elapsed)

                      end

            previous_run.store("#{description}", new_times)
          end

          File.write(previous_runs_filename, previous_run.to_yaml)

        else

          File.write(previous_runs_filename, current_run.to_yaml)

        end
      end

      # Creates a new report file in the 'report_filename'. This file contains
      # a line for each test of the following example format:
      #
      # Avg: 0.0555555 Min: 0.0498765 Max: 0.0612345 Description: The test name
      #
      # Note however the timings are to 9 decimal places, and padded to 12
      # characters and each label is coloured, Avg (yellow), Min (green),
      # Max (red) and Description (blue). It looks pretty!
      #
      # @return [void]
      def create_new_report!
        File.write(report_filename, report_header + report_body)
      end

      # @return [String] A yellow 'Avg:' label.
      def avg_label
        "\e[33mAvg:\e[39m"
      end

      # @return [String] A blue 'Description:' label.
      def des_label
        "\e[34mDescription:\e[39m"
      end

      # @return [String] A red 'Max:' label.
      def max_label
        "\e[31mMax:\e[39m"
      end

      # @return [String] A green 'Min:' label.
      def min_label
        "\e[32mMin:\e[39m"
      end

    end
  end
end