class SpecRunner

def initialize(spec)
  self.spec = spec
  self.stopped = false
  self.bail = false
  self.bailed = false
  self.tag = ''
end

defm set_bail(bail)
  self.bail = bail
end

defm set_tag(tag)
  self.tag = tag
end

defm has_bailed()
  return self.bailed
end

defm start(reporter, stats)
  spec = self.spec
  did_fail = false
  if has_key(spec, 'describe')
    context = self.call_hook('describe')
  elseif has_key(spec, 'spec_name')
    context = spec.spec_name
  else
    context = 'Unknown Spec'
  end
  reporter.on_context_start(context, stats)

  try
    self.call_hook('before')
  catch /.*/
    self.report_hook_error('before', context, reporter, stats)
    self.stopped = true
  end

  for method in keys(spec)
    if self.stopped
      break
    end

    if self.can_run_test_for(method)
      timer = new SpecTimer()
      meta = new SpecMeta(context, method)

      reporter.on_spec_start(meta, stats)
      result = 0

      try
        self.call_hook('before_each')
        timer.start()
        res = call(self.spec[method], [], self.spec)
        timer.stop()
        self.call_hook('after_each')

        meta.set_duration(timer.get_duration())

        stats.inc_passes()
        reporter.on_spec_pass(meta, stats)
      catch /Unknown function.*expect/
        did_fail = true
        stats.inc_failures()
        error = new DSLError('expect() not found, dsl.riml may not be included')
        reporter.on_spec_failure(meta, error, stats)
      catch /Unknown function.*define_matcher/
        did_fail = true
        stats.inc_failures()
        error = new DSLError('define_matcher() not found, dsl.riml may not be included')
        reporter.on_spec_failure(meta, error, stats)
      catch /^AssertionError/
        did_fail = true
        stats.inc_failures()

        " exeption carries the assertion failure message "
        error = new AssertionError(v:exception)
        reporter.on_spec_failure(meta, error, stats)
      catch /.*/
        did_fail = true
        stats.inc_errors()
        error = new VimError(v:exception)
        reporter.on_spec_error(meta, error, stats)
      end

      reporter.on_spec_end(meta, stats)
      :redraw

      if did_fail && self.bail
        self.bailed = true
        break
      end
    end
  end

  try
    unless self.stopped
      self.call_hook('after')
    end
  catch /.*/
    self.report_hook_error('after', context, reporter, stats)
  end

  reporter.on_context_end(context, stats)
end

defm stop
  self.stopped = true
end

" private methods

def report_hook_error(hook, context, reporter, stats)
  meta = new SpecMeta(context, hook)

  reporter.on_spec_start(meta, stats)

  stats.inc_errors()
  error = new VimError(v:exception)
  reporter.on_spec_error(meta, error, stats)
end

def call_hook(hook)
  if has_key(self.spec, hook)
    return call(self.spec[hook], [], self.spec)
  else
    return "Undefined hook: #{hook}"
  end
end

def can_run_test_for(method)
  if self.tag == ''
    return self.is_test_method(method)
  else
    return self.is_test_method(method) && self.is_tagged(method)
  end
end

def is_test_method(method)
  return method =~ '^it'
end

def is_tagged(method)
  return method =~ "_#{self.tag}$"
end

end

class SpecError

def initialize(message)
  self.is_spec_error = true
  self.message = message

  self.exception = v:exception
  self.throwpoint = v:throwpoint
end

defm get_message
  return self.message
end

defm get_stacktrace
  unless has_key(self, 'stacktrace')
    self.build_stacktrace()
  end

  return self.stacktrace
end

def build_stacktrace
  str = substitute(self.throwpoint, '\v(function )', '', '')
  self.stacktrace = split(str, '\.\.')
  self.stacktrace = map(self.stacktrace, '"at " . v:val')
end

end

class DSLError < SpecError

def initialize(message)
  super(message)
  self.is_dsl_error = true
end

end

class AssertionError < SpecError

def initialize(message)
  super(message)
  self.is_assertion_error = true
end

end

class VimError < SpecError

def initialize(message)
  super(message)
  self.is_vim_error = true
end

end