# frozen_string_literal: true

#
# Copyright (C) 2017 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.

require "rspec/core/formatters/base_formatter"
require_relative "../rerun_argument"

# Base formatter that doesn't do anything in and of itself, except gather
# error context-y stuff for subclasses. If you have multiple such
# formatters, it creates/reuses a single ErrorSummary for each example.
#
# Stuff it tracks:
#  * previous specs and their results
#  * rails log during the spec
#  * js errors (selenium)
#  * screenshots (selenium)
#  * video capture (selenium + xvfb)
module ErrorContext
  class BaseFormatter < ::RSpec::Core::Formatters::BaseFormatter
    attr_reader :summary

    def example_started(notification)
      @summary = ErrorSummary.start(notification.example)
    end

    def example_finished(*)
      ErrorSummary.finish
    end

    delegate :errors_path, to: :@summary

    def self.inherited(klass)
      super
      ::RSpec::Core::Formatters.register klass,
                                         :example_started,
                                         :example_failed,
                                         :example_pending,
                                         :example_passed

      # TODO: once https://github.com/rspec/rspec-core/pull/2387 lands,
      # remove this and change the register call to use example_finished.
      # Note that these are not just aliases, since the subclass might
      # redefine example_finished in its body
      klass.class_eval do
        def example_failed(notification)
          example_finished(notification)
        end

        def example_pending(notification)
          example_finished(notification)
        end

        def example_passed(notification)
          example_finished(notification)
        end
      end
    end
  end

  class ErrorSummary
    class << self
      # In the case of loads of specs failing, don't generate error pages
      # beyond a certain point
      MAX_FAILURES_TO_RECORD = 20
      attr_writer :num_failures

      def num_failures
        @num_failures ||= 0
      end

      def start(example)
        @summary ||= begin
          summary = new(example)
          @errors_path = nil
          @base_error_path = nil
          summary.start
          summary
        end
        @summary
      end

      def finish
        return unless @summary

        note_recent_spec_run @summary.example
        @summary.finish
        @summary = nil
      end

      def note_recent_spec_run(example)
        recent_spec_runs << {
          exception: example.exception,
          location: RerunArgument.for(example),
          pending: example.pending,
          recorded_at: Time.zone.now
        }
        self.num_failures += 1 if example.exception
      end

      def discard_remaining?
        num_failures > MAX_FAILURES_TO_RECORD
      end

      def recent_spec_runs
        @recent_spec_runs ||= []
      end

      def base_error_path
        @base_error_path ||= ENV.fetch("ERROR_CONTEXT_BASE_PATH", Rails.root.join("log/spec_failures/Initial"))
      end
    end

    attr_reader :example

    def initialize(example)
      @example = example
    end

    def discard?
      !example.exception || ErrorSummary.discard_remaining?
    end

    def selenium_crashed?
      # this is a common error, there have been many bugs reported to selenium
      # one example bug https://github.com/SeleniumHQ/selenium/issues/14438
      example.exception&.message&.include?("disconnected: not connected to DevTools")
    end

    def selenium?
      return @selenium unless @selenium.nil?

      @selenium = defined?(SeleniumDependencies) && example.example_group.include?(SeleniumDependencies)
    end

    def start
      Rails.logger.capture_messages!
      # TODO: does not work with new docker builds
      # start_capturing_video! if capture_video? && !ErrorSummary.discard_remaining?
    end

    def finish
      if discard?
        # TODO: does not work with new docker builds
        # discard_video! if capturing_video?
      else
        save_screenshot! if capture_screenshot? # rubocop:disable Style/IfInsideElse
        # TODO: does not work with new docker builds
        # save_video! if capturing_video?
      end
    end

    # def capturing_video?
    #   @capturing_video
    # end

    # def start_capturing_video!
    #   @capturing_video = true
    #   SeleniumDriverSetup.headless.video.start_capture
    # end

    def save_screenshot!
      SeleniumDriverSetup.driver.save_screenshot(File.join(errors_path, screenshot_name))
    end

    # def save_video!
    #   SeleniumDriverSetup.headless.video.stop_and_save(File.join(errors_path, screen_capture_name))
    # end

    # def discard_video!
    #   SeleniumDriverSetup.headless.video.stop_and_discard
    # end

    def log_messages
      Rails.logger.captured_messages
    end

    def page_html
      example.metadata[:page_html] || "Page HTML was not captured."
    end

    def capture_screenshot?
      selenium? && !selenium_crashed?
    end

    # def capture_video?
    #   selenium? && SeleniumDriverSetup.run_headless?
    # end

    def js_errors
      return if !selenium? || selenium_crashed?

      @js_errors ||= begin
        js_errors = SeleniumDriverSetup.js_errors

        # ignore "mutating the [[Prototype]] of an object" js errors
        mutating_prototype_error = "mutating the [[Prototype]] of an object"

        js_errors.reject! do |error|
          error["errorMessage"].start_with? mutating_prototype_error
        end

        js_errors
      end
    end

    def spec_path
      ex_name = example.exception ? example.exception.class.name : "none"
      @spec_path ||= RerunArgument.for(example).sub(%r{\A[./]+}, "") + "/#{ex_name}"
    end

    def errors_path
      @errors_path ||= begin
        errors_path = File.join(ErrorSummary.base_error_path, spec_path)
        FileUtils.mkdir_p(errors_path)
        errors_path
      end
    end

    def screenshot_name
      "screenshot.png" if capture_screenshot? && !discard?
    end

    # def screen_capture_name
    #   "capture.mp4" if capture_video? && !discard?
    # end
  end
end
