# FIXME: Is there an easier way to load these other than just force feeding the filenames?
Rtml::Test::SimulatorPostProcessors::CardParsers
Rtml::Test::SimulatorPostProcessors::Submit
Rtml::Test::SimulatorPostProcessors::Receipt

class Rtml::Test::Simulator
  delegate :response, :request, :controller, :to => :session
  delegate :screens, :current_screen, :current_screen_id, :next_screen, :next_screen_id, 
           :waiting_for_input?, :find_screen, :process, :continue, :receipt,
           :to => :app
  attr_reader :app, :session

  # Accepts a path to the TML document which should be requested. To pass raw TML,
  # use this syntax:
  #   Rtml::Test::Simulator.new(:tml => my_tml_code)
  def initialize(start_at = nil)
    @session = ActionController::Integration::Session.new
    @variables = Rtml::Test::VariableScope.new
    @headers = HashWithIndifferentAccess.new
    if start_at.kind_of?(Hash)
      load_tml! start_at[:tml]
    else
      visit(start_at) unless start_at.nil?
    end
  end

  def post_data(location, data = {})
    post(location, data, @headers)
    load_tml!(response.body)
  end

  def header(name, value)
    @headers[name] = value
  end
  
  def headers
    @headers.dup
  end

  # Instead of using "visit", this just force-feeds some TML into the simulator. Useful for
  # hard-coded, quick-and-dirty debug situations, but far less useful for testing a real app
  # or a large document.
  def load_tml!(tml)
    @path = nil
    build_app(tml)
    process
  end

  def process(forward = false, &block)
    # FIXME: TODO: Refactor, this is too ugly!
    if block_given?
      uri = catch(:new_document) { yield; nil }
    else
      uri = catch(:new_document) do
        if forward
          app.continue_forward
        else
          app.continue
        end
        nil
      end
    end

    if uri
      if uri =~ /^tmlvar\:(.*)$/ # it's a TML variable referencing a screen ID
        uri = variables[$~[1]]
        if uri.blank?
          raise Rtml::Errors::SimulationError, "Empty screen reference: tmlvar #{$~[1]} on screen #{current_screen_id}"
        end
      end
      if uri == @path && current_screen_id == app.screens.first['id']
        raise Rtml::Errors::SimulationError,
              "Circular foreign reference: #{@path}##{current_screen_id}"
      end
      visit_uri(uri)
    end

    do_post_processing!
  end

  def visit(path, options = {})
    options.reverse_merge! default_options
    path = "##{path}" if path.kind_of?(Symbol)
    if File.file?(path)
      build_app File.read(@path = path)
    else
      path, screen = path_and_screen_from path
      if !path.blank? && @path != path
        get_and_build(path)
      else
        app.screenflow.clear
      end
      if screen && !screen.blank? && screen != '#'
        app.jump_to_screen screen
      end
    end
    process if options[:process]
  end

  # Visits the specified URI.
  # This merely wraps a call to #visit, but is called internally by #process when following
  # a direction to visit a new URI outside of the current document. This makes it useful for
  # setting up test expectations because you can make sure that #visit_uri is (or isn't)
  # called without worrying about breaking #visit.
  def visit_uri(uri)
    visit(uri)
  end

  def follow_link(path_or_text)
    on_current_screen("a").each do |hyperlink|
      if hyperlink['href'] == path_or_text || hyperlink.inner_text =~ /#{Regexp::escape path_or_text}/i
        visit hyperlink['href']
        return
      end
    end
    raise Rtml::Errors::SimulationError,
          "Hyperlink not found on screen #{current_screen_id.inspect}: #{path_or_text.inspect}"
  end

  def press(which_button)
    process do
      app.trigger_button_press(which_button)
      process # FIXME: is this even necessary?
    end
  end

  # carrier can be any of :visa, :mastercard, :jcb, :amex, :discover, and :debit.
  # if it's anything else, the 'card.pan' and 'card.scheme' options must be specified.
  def swipe_card(carrier, options = {})
    options.reverse_merge!('card.cardholder_name' => 'Cardholder',
                           'card.effective_date' => Time.now,
                           'card.expiry_date' => 1.year.from_now,
                           'card.input_type' => 1,
                           'card.issue_number' => 0,
                           'card.issuer_name' => 'NA',
                           'card.scheme' => carrier.to_s.upcase,
                           'card.pan' => case carrier
                             when :visa then '4111111111111111'
                             when :mastercard then '5454545454545454'
                             when :amex then '3434343434343434'
                             when :discover then '6011000012345678'
                             when :jcb then '3083000012345678'
                             when :debit then '1000000012345678'
                             else nil
                           end
    )

    unless options['card.pan'] && options['card.scheme']
      raise Rtml::Errors::SimulationError, "Carrier not recognized and/or options are invalid"
    end

    card_parsers = on_current_screen("card")
    card_parsers.each do |card|
      if card['parser'] == 'mag' && card['parser_params'] == 'read_data'
        app.update_variables(options)
        process(:forward)
        return
      end
    end
    raise Rtml::Errors::SimulationError,
          "Could not find a suitable card parser (with parser 'mag' and parser_params 'read_data') on screen #{current_screen_id.inspect}"
  end

  def fill_in(variable_name, value)
    variable_name = variable_name.to_s unless variable_name.kind_of?(String)
    on_current_screen("input").each do |field|
      if field['name'] == variable_name
        variables[variable_name] = value
        return
      end
    end
    raise Rtml::Errors::SimulationError,
          "Form element for variable #{variable_name.inspect} does not appear on screen #{current_screen_id.inspect}"
  end

  def variables
    app.variable_scope
  end

  private
  delegate :get, :post, :put, :delete, :to => :session

  def default_options
    {
      :process => true
    }
  end
  
  def on_current_screen(field_name)
    current_screen ? ((current_screen / field_name) || []) :
            raise(Rtml::Errors::SimulationError, "Current screen was expected to not be nil")
  end

  def build_app(tml)
    @app = Rtml::Test::TmlApplication.new(tml)
  end

  def get_and_build(path)
    # set up state mocking
    unless path =~ /ignore_state/
      if path =~ /\?/
        path.concat "&ignore_state=true"
      else
        path.concat "?ignore_state=true"
      end
    end

    get @path = path, {}, @headers
    if response # actually the only time there's no response should be during tests which stub #get
      format = response.template.template_format
      unless format == :rtml
        raise Rtml::Errors::InvalidFormat, "Response must be in :rtml format; found #{format.inspect}:\n#{response.body}"
      end
      build_app(response.body)
    end
  end

  def path_and_screen_from(path)
    path, screen = path.split(/#/)
    # query string
    if screen =~ /\?/
      screen, query = screen.split(/\?/)
      path = "#{path}?#{query}"
    end
    [path, screen]
  end

  include Rtml::Widgets
  
  # we're almost done. App is likely in a "waiting" state; we need to see if it's "waiting"
  # for something that the simulator can, er, simulate, such as card risk management or
  # EMV processing. If it is, then we need to fill out whatever variables are expected, and
  # then send another call to #process.
  def do_post_processing!
    return unless current_screen
    self.widget_entry_points.each do |entry_point|
      send(entry_point)
    end
  end
end