module HyperSpec
  # Defines a series of methods that will build a test page
  # This module is typically included into the HyperSpecTestController class.
  module ControllerHelpers
    # These methods are dependent on the stack being used.  See the
    # RailsControllerHelpers module and rack.rb for two implementations.
    # Each method should append the appropriate code to the @page array

    # return an empty 204 status either by setting headers or
    # returning and appropriate response for rack.
    def ping!
      raise 'must implement'
    end

    def json!
      # this can be a no-op but if json is not included by the application,
      # hyper-spec will fail, with an error complaining about to_json
    end

    # return a script or style_sheet tag pointing to some asset.
    # typically you be pointing to a path on the server or using
    # sprockets to deliver the file.

    def require!(_file_)
      raise 'must implement'
    end

    def style_sheet!(_file_)
      raise 'must implement'
    end

    # deliver the page.  The @page variable will contain the html ready to go,
    # any additional options that are passed through from the spec will be
    # in the @render_params variable.  For example layout: 'my_special_layout'

    def deliver!
      raise 'must implement'
    end

    # generate a react_render top level block.  This will only be called if
    # you use the mount directive in your specs, so it is optional.

    def mount_component!
      raise 'mount_component not implemented in HyperSpecTestController'
    end

    # by default the route back to the controller will be the controller name, less the
    # word Controller, and underscored.  If you want some other name redefine this
    # method in the HyperSpecController class.

    def self.included(base)
      def base.route_root
        # Implement underscore without using rails underscore, so we don't have a
        # dependency on ActiveSupport
        name.gsub(/Controller$/, '')
            .gsub(/::/, '/')
            .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
            .gsub(/([a-z\d])([A-Z])/, '\1_\2')
            .tr('-', '_')
            .downcase
      end
    end

    # The remainder of the methods should work for most implementations.

    # helper method checking the render_on parameter

    def on_client?
      @render_on != :server_only
    end

    # The controllers behavior is kept as an array of values in the Controller cache
    # under a unique id for each test run.  Grab the parameters and move them to instance vars

    # If this is just a ping! Then we can just exit with nil.

    def initialize!
      return if params[:id] == 'ping'

      key =               "/#{self.class.route_root}/#{params[:id]}"
      test_params =       Internal::Controller.cache_read(key)

      @component_name =   test_params[0]
      @component_params = test_params[1]
      @html_block =       test_params[2]
      @render_params =    test_params[3]
      @render_on =        @render_params.delete(:render_on) || :client_only
      @_mock_time =       @render_params.delete(:mock_time)
      @style_sheet =      @render_params.delete(:style_sheet)
      @javascript =       @render_params.delete(:javascript)
      @code =             @render_params.delete(:code)

      @page = ['<body>']
    end

    # add any html code generated by the insert_html directive

    def html_block!
      @page << @html_block
    end

    # patch behavior of the HyperComponent TopLevelRailsComponent class
    # so that things like events are passed back to the test harness
    TOP_LEVEL_COMPONENT_PATCH =
      Opal.compile(File.read(File.expand_path('../sources/top_level_rails_component.rb', __dir__)))

    # patch time cop and lolex so they stay in sync across the client and server
    TIME_COP_CLIENT_PATCH =
      Opal.compile(File.read(File.expand_path('../hyper-spec/internal/time_cop.rb', __dir__))) +
      "\n#{File.read(File.expand_path('../sources/lolex.js', __dir__))}"

    def client_code!
      if @component_name
        @page << "<script type='text/javascript'>\n#{TOP_LEVEL_COMPONENT_PATCH}\n</script>"
      end
      @page << "<script type='text/javascript'>\n#{@code}\n</script>" if @code
    end

    def time_cop_patch!
      @page << "<script type='text/javascript'>\n#{TIME_COP_CLIENT_PATCH}\n</script>"
    end

    # Add the go_function to the client console.  This is used to stop a hyper-spec pause directive.

    def go_function!
      @page << "<script type='text/javascript'>go = function() "\
               '{window.hyper_spec_waiting_for_go = false}</script>'
    end

    # First lines displayed on the console will be the name of the spec

    def example_title!
      title = Internal::Controller.current_example_description!
      @page << "<script type='text/javascript'>console.log('%c#{title}',"\
               "'color:green; font-weight:bold; font-size: 200%')</script>"
    end

    # generate each piece of the page, and then deliver it

    def style_sheet_file
      @style_sheet || (!@render_params[:layout] && 'application')
    end

    def application_file
      @javascript || (on_client? && !@render_params[:layout] && 'application')
    end

    def test
      return ping! unless initialize!

      html_block!
      example_title!                 if Internal::Controller.current_example
      go_function!                   if on_client?
      style_sheet!(style_sheet_file) if style_sheet_file
      application!(application_file) if application_file
      json!
      time_cop_patch!                if on_client? || Lolex.initialized?
      client_code!                   if on_client?
      mount_component!               if @component_name
      @page = @page.join("\n") + "\n</body>\n"
      deliver!
    end
  end
end
