RSpec::Support.require_rspec_core "formatters/base_text_formatter"
RSpec::Support.require_rspec_core "formatters/exception_presenter"
require './core/utils/post_build_utils'
require './core/utils/stats_utils.rb'
require './core/utils/summary_writer_utils'
require './core/configs/grid_config'
require 'smoca_common/utils/twitch_utils'
require 'smoca_common/utils/screenshot_utils'

module RSpec
  module Core
    module Formatters
      class ParallelOutput < BaseTextFormatter
        # RSpec allows for customization of output via Formatters
        # This is a custom formatter which will make the following changes:
        # - Example Group and Scenario write to one line
        # - Failed Example includes a screenshot of the failure
        # - Dump Summary & Pending Never Display
        # - Dump Failures will write to a file, and display at the end

        # This is primarily useful in Parallel situations which are testing and outputting
        # multiple groups simultaneously

        include PostBuildUtils
        include StatUtils
        include RSpec::Core::Formatters::ConsoleCodes # Used for coloring messages
        include SummaryWriterUtils # Used to write summary contents to file
        include Tracking # Used for measuring process durations
        include GridConfig

        Formatters.register self, :example_passed, :example_pending, :example_failed,
          :dump_failures, :dump_pending, :dump_summary

        def initialize(output)
          super
          @group_level = 0
        end

        # @param passed - object type ExampleNotification
        # Puts the output of the Rspec Example to STOUT immediately when it passes
        # Calls on passed_output method to further build this string
        def example_passed(passed)
          output.puts passed_output(passed.example)
        end

        # @param pending - object type SkippedExampleNotification
        # Puts the output of the RSpec Example Pending to STOUT immediately after reaching this pending example
        # Calls on pending_output method to further build this string
        def example_pending(pending)
          output.puts pending_output(pending.example,
            pending.example.execution_result.pending_message)

          write_pending(build_pending_example_output(pending.example)) # Write the pending example to a CSV to display during postbuild
        end

        # @param failure - object type FailedExampleNotification
        # Puts the output of the RSpec Example Failure to STOUT immediately upon failure
        # Calls on failure_output method to further build this string
        def example_failed(failure)
          if failure.exception.message.include?('Net::ReadTimeout') # Measure the amount of Net::ReadTimeout's
            increment('warnings.net_read_timeout')
          end

          output.puts failure_output(failure.example,
            failure.example.execution_result.exception)

          write_failure(build_failed_example_output(failure)) # Write the Failure to a CSV to display during postbuild
        end

        # @param notification - object type SummaryNotification
        def dump_summary(notification)
          # Display No Output

          # Output results to Statsd for each process
          measure_process_duration(TwitchUtils.thread_number, notification) if ENV['TRACKING'] == 'true'

          # Write the duration to a CSV to display during postbuild
          write_test_duration(notification.duration)
        end

        # @param notification - object type ExamplesNotification
        def dump_pending(notification)
          # Intentionally left empty - display no output
        end

        # @param notification - object type ExamplesNotification
        # Write a file with all of the failures dumped. This is intended to be accessible post-run
        def dump_failures(notification)
          # Intentionally left empty - display no output initially
          # All of the failures are being logged as they arrise
          # A post-build task will display them
        end

        private

        # @param example - object type Example for the example that passed
        # @return a built string to be displayed when an example passes
        def passed_output(example)
          # Output as Example Group: Example Description
          ConsoleCodes.wrap(
            "#{current_indentation}#{example.full_description.strip} - (Process #{TwitchUtils.thread_number} - Port #{Capybara.server_port})",
            :success
          )
        end

        # @param example - object type Example for the example that is pending
        # @return a built string to be displayed when an example is pending
        def pending_output(example, message)
          # Output as Example Group: Example Description (PENDING <reason>)
          ConsoleCodes.wrap(
            "#{current_indentation}#{example.full_description.strip}" \
            " (PENDING: #{message}) - (Process #{TwitchUtils.thread_number} - Port #{Capybara.server_port})",
            :pending
          )
        end

        # @param example - object type Example for the example that failed
        # @return string, a built string to be displayed when an example fails
        def failure_output(example, _exception)
          # Output as Example Group: Example Description (FAILED - <number)
          string = ConsoleCodes.wrap(
            "#{current_indentation}#{example.full_description.strip} " \
            "(FAILED - #{next_failure_index}). - (Process #{TwitchUtils.thread_number} - Port #{Capybara.server_port})",
            :failure
          )

          return string
        end

        # A running count of the failure number
        def next_failure_index
          @next_failure_index ||= 0
          @next_failure_index += 1
        end

        def current_indentation
          '  ' * @group_level
        end

        # @param failure - object type FailedExampleNotification
        # @return A built string that can be used to print out a formatted failure
        def build_failed_example_output(failure)
          # Format:
          #
          # INDEX) Full Example Description
          # Failure/Exception Information
          # Screenshot
          # File Location
          string = failure.fully_formatted('') # Pass in a blank string, as this method requires a number.
          # However, with other Parallel processes, the numbers will repeat. Leave the number to the file outputter

          # Add screenshot data. Dig will return nil if it doesn't exist
          screenshot = failure.example.metadata.dig(:screenshot, :url)
          html = failure.example.metadata.dig(:screenshot, :html_url)

          # Convert from the Raw S3 URL to the S3 Proxy URL
          screenshot_display_url = ScreenshotUtils.convert_s3_url(screenshot)
          html_display_url = ScreenshotUtils.convert_s3_url(html)

          # Add those to the output string
          string << wrap("\n\tScreenshot: #{screenshot_display_url}", :red) unless screenshot_display_url.nil?
          string << wrap("\n\tHTML: #{html_display_url}", :red) unless html_display_url.nil? # Append if there is a screenshot

          return string
        end

        # @param example - object type Example
        # @return A built string that can be used to print out a formatted pending example
        def build_pending_example_output(example)
          string = ''
          string << ConsoleCodes.wrap("#{example.full_description}\n", :pending)
          string << ConsoleCodes.wrap("\t# #{example.metadata[:execution_result].pending_message}\n", :cyan)
          string << ConsoleCodes.wrap("\t# #{example.location}", :cyan)

          return string
        end
      end
    end
  end
end
