module Riot
  module ContextHelpers

    # Add a setup block.
    #
    # A setup block defines the topic of the context. There can be multiple setup
    # blocks; each can access the previous topic through the +topic+ attribute.
    #
    #   context "A string" do
    #     setup { "foo" }
    #     setup { topic * 2 }
    #     asserts(:length).equals(6)
    #   end
    #
    # If you provide +true+ as the first argument, the setup will be unshifted onto the list of setups,
    # ensuring it will be run before any other setups. This is really only useful for context middlewares.
    #
    # @param [Boolean] premium indicates importance of the setup
    # @return [Riot::Setup]
    def setup(premium=false, &definition)
      setup = Setup.new(&definition)
      premium ? @setups.unshift(setup) : @setups.push(setup)
      setup
    end

    # Helpers are essentially methods accessible within a situation.
    #
    # They're not setup methods, but can be called from setups or from assetions. Each time called, the
    # helper will be evaluated. It's not currently memoized.
    #
    #   context "A string" do
    #     helper(:foo) { "bar" }
    #     asserts("a foo") { foo }.equals("bar")
    #   end
    #
    # @param [String, Symbol] name the name of the helper
    # @return [Riot::Helper]
    def helper(name, &block)
      (@setups << Helper.new(name, &block)).last
    end

    # A setup shortcut that returns the original topic so you don't have to. Good for nested setups. Instead
    # of doing this in your context:
    #
    #   setup do
    #     topic.do_something
    #     topic
    #   end
    #
    # You would do this:
    #
    #   hookup { topic.do_something } # Yay!
    #
    # @return [Riot::Setup]
    def hookup(&definition)
      setup { self.instance_eval(&definition); topic }
    end

    # Add a teardown block. You may define multiple of these as well.
    #
    #  teardown { Bombs.drop! }
    #
    # @return [Riot::Setup]
    def teardown(&definition)
      (@teardowns << Setup.new(&definition)).last
    end

    # Makes an assertion.
    #
    # In the most basic form, an assertion requires a descriptive name and a block.
    #
    #   asserts("#size is equals to 2") { topic.size == 2 }
    #
    # However, several shortcuts are available. Assertion macros can be added to the
    # end, automating a number of common assertion patterns, e.g.
    #
    #   asserts("#size") { topic.size }.equals(2)
    #
    # Furthermore, the pattern of testing an attribute on the topic is codified as
    #
    #   asserts(:size).equals(2)
    #
    # Or with arguments:
    #
    #   asserts(:foo,1,2).equals(3)
    #
    # Passing a Symbol to +asserts+ enables this behaviour. For more information on
    # assertion macros, see {Riot::AssertionMacro}.
    #
    # @param [String, Symbol] what description of test or property to inspect on the topic
    # @return [Riot::Assertion]
    def asserts(*what, &definition)
      new_assertion("asserts", *what, &definition)
    end

    # Same as #asserts, except it uses the phrase "should" in the report output. Sometimes you feel like a
    # nut, sometimes you don't.
    #
    #   should("ensure expected") { "bar" }.equals("bar")
    #
    #   #should also has the same shortcuts available to #asserts:
    #
    #   should(:bar,1,2).equals(3)
    #
    # @param [String, Symbol] what description of test or property to inspect on the topic
    # @return [Riot::Assertion]
    def should(*what, &definition)
      new_assertion("should", *what, &definition)
    end

    # Like an assertion, but expects negative results.
    #
    # In the most basic form, a denial requires a descriptive name and a block.
    #
    #   denies("#size is equals to 2") { topic.size != 2 }
    #
    # Several shortcuts are available here as well. Assertion macros can be added to the
    # end, automating a number of common assertion patterns, e.g.
    #
    #   denies("#size") { topic.size }.equals(2)
    #
    # Furthermore, the pattern of testing an attribute on the topic is codified as
    #
    #   denies(:size).equals(2)
    #
    # the shorcut can also pass additional arguments to the method like:
    #
    #   denies(:foo,1,3).equals(2)
    #
    # Passing a Symbol to +denies+ enables this behaviour. For more information on
    # assertion macros, see {Riot::AssertionMacro}.
    #
    # @param [String, Symbol] what description of test or property to inspect on the topic
    # @return [Riot::Assertion]
    def denies(*what, &definition)
      what << {:negative => true}
      new_assertion "denies", *what, &definition
    end

    # This is the negative form of #should. This is exactly like denies. Just here for syntactic sugar.
    #
    # A basic eample is:
    #
    #   should_not("have size equal 2") { topic.size == 2 }
    #
    # In addition, the #denies shortcut as available as well:
    #
    #   should_not(:size).equals 3
    #
    # Or passing in arguments
    #
    #   should_not(:foo,1,2).equals(2)
    #
    # @param [String,Symbol] what description or property to inspect on the topic
    # @return [Riot::Assertion]
    def should_not(*what, &definition)
      what << {:negative => true}
      new_assertion "should not", *what, &definition
    end

    # Makes an assertion on the topic itself, e.g.
    #
    #   asserts_topic.matches(/^ab+/)
    #
    # @param [String] what description of test
    # @return [Riot::Assertion]
    def asserts_topic(what="that it")
      asserts(what) { topic }
    end

    # Makes a negative assertion on the topic itself, e.g.
    #
    #   denies_topic.matches(/^ab+/)
    #
    # @param [String] what description of test
    # @return [Riot::Assertion]
    def denies_topic(what="that it")
      denies(what) { topic }
    end
  private

    def new_assertion(scope, *args, &definition)
      options = args.extract_options!
      definition ||= proc { topic.send(*args) }
      description = "#{scope} #{args.first}"
      description << " with arguments(s): #{args.slice(1, args.length).inspect}" if args.size > 1
      (@assertions << assertion_class.new(description, options[:negative], &definition)).last
    end

  end # AssertionHelpers
end # Riot
